fernotify 1.2.1 → 1.2.8

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.
@@ -1,49 +1,36 @@
1
- /**
2
- * Sistema de Notificaciones Modernas
3
- * Librería ligera de notificaciones con animaciones fluidas
4
- * Dependencias automáticas: anime.js, Boxicons
5
- */
6
-
7
- (function ensureAnimeDependency() {
8
- if (typeof anime !== 'undefined') {
9
- initFerNotify();
10
- } else {
11
- const script = document.createElement('script');
12
- script.src = 'https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js';
13
- script.onload = initFerNotify;
14
- script.onerror = () => {
15
- console.error('FerNotify: No se pudo cargar anime.js. Por favor, cargalo manualmente.');
16
- };
17
- document.head.appendChild(script);
18
- }
19
-
20
- function initFerNotify() {
21
- class NotificationSystem {
22
- constructor() {
23
- this.currentNotification = null;
24
- this._lastActiveElement = null;
25
- this._currentLoadingPromise = null;
26
- this.injectStyles();
27
- this.loadBoxicons();
28
- }
29
-
30
- /**
31
- * Cargar Boxicons CSS si no está presente
32
- */
33
- loadBoxicons() {
34
- if (!document.querySelector('link[href*="boxicons"]')) {
35
- const link = document.createElement('link');
36
- link.rel = 'stylesheet';
37
- link.href = 'https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css';
38
- document.head.appendChild(link);
39
- }
40
- }
41
-
42
- /**
43
- * Inyectar estilos CSS
44
- */
45
- injectStyles() {
46
- const style = document.createElement('style');
1
+ "use strict";
2
+ (function ensureAnimeDependency() {
3
+ if (typeof anime !== 'undefined') {
4
+ initFerNotify();
5
+ }
6
+ else {
7
+ const script = document.createElement('script');
8
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js';
9
+ script.onload = initFerNotify;
10
+ script.onerror = () => {
11
+ console.error('FerNotify: No se pudo cargar anime.js. Por favor, cargalo manualmente.');
12
+ };
13
+ document.head.appendChild(script);
14
+ }
15
+ function initFerNotify() {
16
+ class NotificationSystem {
17
+ constructor() {
18
+ this.currentNotification = null;
19
+ this._lastActiveElement = null;
20
+ this._currentLoadingPromise = null;
21
+ this.injectStyles();
22
+ this.loadBoxicons();
23
+ }
24
+ loadBoxicons() {
25
+ if (!document.querySelector('link[href*="boxicons"]')) {
26
+ const link = document.createElement('link');
27
+ link.rel = 'stylesheet';
28
+ link.href = 'https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css';
29
+ document.head.appendChild(link);
30
+ }
31
+ }
32
+ injectStyles() {
33
+ const style = document.createElement('style');
47
34
  style.textContent = `
48
35
  .notification-overlay {
49
36
  position: fixed;
@@ -213,6 +200,14 @@
213
200
  .notification-icon.info::before {
214
201
  background: #3b82f6;
215
202
  }
203
+ .notification-icon.question {
204
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
205
+ color: white;
206
+ }
207
+
208
+ .notification-icon.question::before {
209
+ background: #3b82f6;
210
+ }
216
211
 
217
212
  .notification-title {
218
213
  font-size: 24px;
@@ -249,6 +244,15 @@
249
244
  transform: translateY(0);
250
245
  }
251
246
 
247
+ /* group container for multiple action buttons */
248
+ .notification-button-group {
249
+ display: flex;
250
+ gap: 12px;
251
+ justify-content: center;
252
+ flex-wrap: wrap;
253
+ margin-top: 10px;
254
+ }
255
+
252
256
  .notification-icon-checkmark {
253
257
  animation: checkmark-draw 0.6s ease-in-out;
254
258
  }
@@ -320,583 +324,517 @@
320
324
  .dark .notification-loading-text {
321
325
  color: #cbd5e1;
322
326
  }
323
- `;
324
- document.head.appendChild(style);
325
- }
326
-
327
- /**
328
- * Obtener ícono según el tipo
329
- */
330
- getIcon(type) {
331
- // Return a Boxicons markup string — presentation/index.php now imports the CSS.
332
- const icons = {
333
- 'success': '<i class="bx bx-check" aria-hidden="true"></i>',
334
- 'error': '<i class="bx bx-x" aria-hidden="true"></i>',
335
- 'warning': '<i class="bx bx-error" aria-hidden="true"></i>',
336
- 'info': '<i class="bx bx-info-circle" aria-hidden="true"></i>'
337
- };
338
- return icons[type] || icons.info;
339
- }
340
-
341
- /**
342
- * Obtener título por defecto según el tipo
343
- */
344
- getDefaultTitle(type) {
345
- const titles = {
346
- 'success': '¡Éxito!',
347
- 'error': 'Error',
348
- 'warning': 'Advertencia',
349
- 'info': 'Información'
350
- };
351
- return titles[type] || 'Notificación';
352
- }
353
-
354
- /**
355
- * Obtener gradiente de botón según el tipo
356
- */
357
- getButtonGradient(type) {
358
- const gradients = {
359
- 'success': 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
360
- 'error': 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
361
- 'warning': 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
362
- 'info': 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)'
363
- };
364
- return gradients[type] || gradients.info;
365
- }
366
-
367
- /**
368
- * Obtener color de sombra según el tipo
369
- */
370
- getButtonShadow(type) {
371
- const shadows = {
372
- 'success': 'rgba(16, 185, 129, 0.4)',
373
- 'error': 'rgba(239, 68, 68, 0.4)',
374
- 'warning': 'rgba(245, 158, 11, 0.4)',
375
- 'info': 'rgba(59, 130, 246, 0.4)'
376
- };
377
- return shadows[type] || shadows.info;
378
- }
379
-
380
- /**
381
- * Mostrar notificación
382
- *
383
- * @param {Object} options - Opciones de la notificación
384
- * @param {string} options.type - Tipo: 'success', 'error', 'warning', 'info'
385
- * @param {string} options.title - Título (opcional, usa título por defecto)
386
- * @param {string} options.message - Mensaje a mostrar
387
- * @param {string} options.buttonText - Texto del botón (opcional, default: 'OK')
388
- * @param {string} options.buttonColor - Color del botón en formato CSS (opcional, usa color del tipo por defecto)
389
- * @param {Function} options.onClose - Callback al cerrar (opcional)
390
- * @param {number} options.timer - Auto-cerrar después de X ms (opcional)
391
- * @param {boolean} options.allowOutsideClick - Permitir cerrar haciendo click fuera (default: true)
392
- * @param {boolean} options.allowEscapeKey - Permitir cerrar con tecla ESC (default: true)
393
- */
394
- show(options = {}) {
395
- // Cerrar notificación existente si hay (esperar a que termine)
396
- if (this.currentNotification) {
397
- // Asegurar que se elimine completamente antes de continuar
398
- const oldOverlay = this.currentNotification;
399
- this.currentNotification = null;
400
- try {
401
- if (oldOverlay && oldOverlay.parentNode) {
402
- oldOverlay.parentNode.removeChild(oldOverlay);
403
- }
404
- } catch (e) { }
405
- }
406
-
407
- const {
408
- type = 'info',
409
- title = this.getDefaultTitle(type),
410
- message = '',
411
- buttonText = 'OK',
412
- buttonColor = null,
413
- onClose = null,
414
- timer = null,
415
- allowOutsideClick = true,
416
- allowEscapeKey = true,
417
- // New option: hideButton true -> do not render action button
418
- hideButton = false
419
- } = options;
420
-
421
- // Option to show a small close 'X' in the corner
422
- const showCloseButton = options.showCloseButton === true;
423
-
424
- // Bloquear scroll del body y root (más robusto)
425
- try { document.body.style.overflow = 'hidden'; } catch (e) { }
426
- try { document.documentElement.style.overflow = 'hidden'; } catch (e) { }
427
-
428
- // Crear overlay
429
- const overlay = document.createElement('div');
430
- overlay.className = 'notification-overlay';
431
- // Accessibility: make overlay focusable and a dialog
432
- overlay.tabIndex = -1;
433
- overlay.setAttribute('role', 'dialog');
434
- overlay.setAttribute('aria-modal', 'true');
435
- // Ensure overlay receives pointer events
436
- overlay.style.pointerEvents = 'auto';
437
-
438
- // Crear box
439
- const box = document.createElement('div');
440
- box.className = 'notification-box';
441
-
442
- // Crear ícono
443
- const icon = document.createElement('div');
444
- icon.className = `notification-icon ${type}`;
445
-
446
- // Si la opción hideButton es true Y message contiene patrón de loading, mostrar spinner
447
- if (hideButton && type === 'info') {
448
- // Mostrar spinner para notificación de carga
449
- icon.className = 'notification-loading-container';
450
- icon.innerHTML = '<div class="notification-spinner"></div>';
451
- icon.style.background = 'transparent';
452
- icon.style.boxShadow = 'none';
453
- // Mantener tamaño adecuado para el spinner
454
- icon.style.width = '100px';
455
- icon.style.height = '100px';
456
- } else {
457
- icon.innerHTML = this.getIcon(type);
458
- }
459
-
460
- // Crear título
461
- const titleElement = document.createElement('h3');
462
- titleElement.className = 'notification-title';
463
- titleElement.textContent = title;
464
-
465
- // Crear mensaje
466
- const messageElement = document.createElement('p');
467
- messageElement.className = 'notification-message';
468
- messageElement.textContent = message;
469
-
470
- // Custom content support: options.html (string) or options.content (HTMLElement)
471
- let customContent = null;
472
- if (options.html || options.content) {
473
- customContent = document.createElement('div');
474
- customContent.className = 'notification-content';
475
- if (options.html) {
476
- try { customContent.innerHTML = options.html; } catch (e) { customContent.textContent = options.html; }
477
- } else if (options.content && options.content instanceof HTMLElement) {
478
- customContent.appendChild(options.content);
479
- }
480
- }
481
-
482
- // Crear (opcional) botón. Si hideButton === true o buttonText es falsy, no renderizamos.
483
- let button = null;
484
- if (!hideButton && buttonText) {
485
- button = document.createElement('button');
486
- button.className = 'notification-button';
487
- button.textContent = buttonText;
488
-
489
- // Aplicar color del botón (personalizado o automático según tipo)
490
- const finalButtonColor = buttonColor || this.getButtonGradient(type);
491
- const buttonShadowColor = this.getButtonShadow(type);
492
- button.style.background = finalButtonColor;
493
- button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
494
- }
495
-
496
- // Close 'X' button in corner (optional)
497
- let closeBtn = null;
498
- if (showCloseButton) {
499
- closeBtn = document.createElement('button');
500
- closeBtn.setAttribute('aria-label', 'Cerrar');
501
- closeBtn.className = 'notification-close';
502
- closeBtn.innerHTML = '&times;';
503
- closeBtn.addEventListener('click', (e) => {
504
- e.stopPropagation();
505
- closeHandler();
506
- });
507
- }
508
-
509
- // Ensamblar
510
- box.appendChild(icon);
511
- // If customContent provided, prefer it. Otherwise render title/message as before.
512
- if (customContent) {
513
- // For accessibility, link aria-describedby to the content
514
- const descId = 'notify-desc-' + Date.now();
515
- customContent.id = descId;
516
- overlay.setAttribute('aria-describedby', descId);
517
- box.appendChild(customContent);
518
- } else {
519
- box.appendChild(titleElement);
520
- box.appendChild(messageElement);
521
- }
522
- // Append close button last so it's visually on top
523
- if (closeBtn) box.appendChild(closeBtn);
524
- if (button) box.appendChild(button);
525
- overlay.appendChild(box);
526
- document.body.appendChild(overlay);
527
-
528
- // Provide a Promise that resolves when this notification is closed.
529
- const closePromise = new Promise((resolveClose) => {
530
- try { overlay._externalResolve = resolveClose; } catch (e) { /* ignore */ }
531
- });
532
-
533
- // If page provides a live region, update it for screen readers
534
- try {
535
- const live = document.getElementById('notify-live');
536
- if (live) {
537
- live.textContent = `${title}: ${message}`;
538
- }
539
- } catch (e) { }
540
-
541
- // Save current focused element to restore later
542
- try { this._lastActiveElement = document.activeElement; } catch (e) { this._lastActiveElement = null; }
543
-
544
- this.currentNotification = overlay;
545
-
546
- // Move focus into the notification for accessibility
547
- // Focus management: focus first focusable element inside box, otherwise the button, otherwise overlay
548
- try {
549
- const focusable = box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])');
550
- if (focusable && focusable.length) {
551
- focusable[0].focus();
552
- } else if (button) {
553
- button.focus();
554
- } else {
555
- overlay.focus();
556
- }
557
- } catch (e) { try { overlay.focus(); } catch (err) { } }
558
-
559
- // Implement focus trap (Tab/Shift+Tab) inside the box
560
- const focusTrap = (e) => {
561
- if (e.key !== 'Tab') return;
562
- const focusable = Array.from(box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'))
563
- .filter(el => el.offsetParent !== null);
564
- if (!focusable.length) {
565
- e.preventDefault();
566
- return;
567
- }
568
- const first = focusable[0];
569
- const last = focusable[focusable.length - 1];
570
- if (!e.shiftKey && document.activeElement === last) {
571
- e.preventDefault();
572
- first.focus();
573
- } else if (e.shiftKey && document.activeElement === first) {
574
- e.preventDefault();
575
- last.focus();
576
- }
577
- };
578
- overlay._focusTrap = focusTrap;
579
- document.addEventListener('keydown', focusTrap);
580
-
581
- // Allow optional animation overrides via options.anim
582
- const anim = options.anim || {};
583
- const overlayDuration = typeof anim.overlayDuration === 'number' ? anim.overlayDuration : 150;
584
- const overlayEasing = anim.overlayEasing || 'easeOutQuad';
585
- const boxDuration = typeof anim.boxDuration === 'number' ? anim.boxDuration : 200;
586
- const boxDelay = typeof anim.boxDelay === 'number' ? anim.boxDelay : 50;
587
- const boxEasing = anim.boxEasing || 'easeOutBack';
588
- const boxStartScale = typeof anim.boxStartScale === 'number' ? anim.boxStartScale : 0.8;
589
- const iconDuration = typeof anim.iconDuration === 'number' ? anim.iconDuration : 250;
590
- const iconDelay = typeof anim.iconDelay === 'number' ? anim.iconDelay : 100;
591
- const iconRotate = (typeof anim.iconRotate === 'number') ? anim.iconRotate : (type === 'success' ? -90 : type === 'error' ? 90 : 0);
592
- if (typeof anim.overlayOpacity === 'number') {
593
- overlay.style.backgroundColor = `rgba(0,0,0,${anim.overlayOpacity})`;
594
- }
595
-
596
- // Animación de entrada con anime.js - configurable
597
- anime({
598
- targets: overlay,
599
- opacity: [0, 1],
600
- duration: overlayDuration,
601
- easing: overlayEasing
602
- });
603
-
604
- anime({
605
- targets: box,
606
- scale: [boxStartScale, 1],
607
- opacity: [0, 1],
608
- duration: boxDuration,
609
- easing: boxEasing,
610
- delay: boxDelay
611
- });
612
-
613
- anime({
614
- targets: icon,
615
- scale: [0, 1],
616
- rotate: [iconRotate, 0],
617
- duration: iconDuration,
618
- easing: boxEasing,
619
- delay: iconDelay
620
- });
621
-
622
- // Efecto hover y listener del botón (solo si existe)
623
- const closeHandler = () => {
624
- this.close(onClose);
625
- };
626
- if (button) {
627
- const buttonShadowColor = this.getButtonShadow(type);
628
- button.addEventListener('mouseenter', () => {
629
- button.style.boxShadow = `0 6px 16px ${buttonShadowColor}`;
630
- });
631
- button.addEventListener('mouseleave', () => {
632
- button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
633
- });
634
- button.addEventListener('click', (e) => {
635
- e.stopPropagation(); // Evitar que el evento llegue al overlay
636
- e.preventDefault();
637
- closeHandler();
638
- });
639
- }
640
-
641
- // Click en overlay para cerrar (solo si está permitido)
642
- if (allowOutsideClick) {
643
- overlay.addEventListener('click', (e) => {
644
- // Close when clicking outside the box (more robust than e.target === overlay)
645
- if (!box.contains(e.target)) {
646
- closeHandler();
647
- }
648
- });
649
- }
650
-
651
- // Auto-cerrar si hay timer
652
- if (timer) {
653
- setTimeout(() => {
654
- closeHandler();
655
- }, timer);
656
- }
657
-
658
- // Tecla ESC para cerrar (solo si está permitido)
659
- if (allowEscapeKey) {
660
- const escHandler = (e) => {
661
- if (e.key === 'Escape') {
662
- closeHandler();
663
- document.removeEventListener('keydown', escHandler);
664
- }
665
- };
666
- // Store handler reference on overlay so close() can remove it if needed
667
- overlay._escHandler = escHandler;
668
- document.addEventListener('keydown', escHandler);
669
- }
670
-
671
- // Return a Promise that resolves when the notification is closed
672
- return closePromise;
673
- }
674
-
675
- /**
676
- * Cerrar notificación actual
677
- */
678
- close(callback = null) {
679
- if (!this.currentNotification) {
680
- return Promise.resolve();
681
- }
682
-
683
- const overlay = this.currentNotification;
684
- const box = overlay.querySelector('.notification-box');
685
-
686
- // CRÍTICO: Limpiar la referencia INMEDIATAMENTE para evitar conflictos
687
- this.currentNotification = null;
688
-
689
- // Animación de salida
690
- anime({
691
- targets: box,
692
- scale: 0.8,
693
- opacity: 0,
694
- duration: 100,
695
- easing: 'easeInQuad'
696
- });
697
-
698
- return new Promise((resolve) => {
699
- anime({
700
- targets: overlay,
701
- opacity: 0,
702
- duration: 100,
703
- easing: 'easeInQuad',
704
- complete: () => {
705
- // Remove keydown handler if present
706
- try {
707
- if (overlay && overlay._escHandler) {
708
- document.removeEventListener('keydown', overlay._escHandler);
709
- overlay._escHandler = null;
710
- }
711
- } catch (e) { }
712
-
713
- // Remove focus trap if present
714
- try {
715
- if (overlay && overlay._focusTrap) {
716
- document.removeEventListener('keydown', overlay._focusTrap);
717
- overlay._focusTrap = null;
718
- }
719
- } catch (e) { }
720
-
721
- // Resolve external Promise returned by show(), if present
722
- try {
723
- if (overlay && typeof overlay._externalResolve === 'function') {
724
- try { overlay._externalResolve(); } catch (er) { }
725
- overlay._externalResolve = null;
726
- }
727
- } catch (e) { }
728
-
729
- // CRÍTICO: Asegurar eliminación completa del overlay
730
- try {
731
- if (overlay && overlay.parentNode) {
732
- overlay.parentNode.removeChild(overlay);
733
- }
734
- } catch (e) {
735
- try { overlay.remove(); } catch (er) { }
736
- }
737
-
738
- // Restaurar scroll SOLO si NO hay otra notificación activa
739
- if (!this.currentNotification) {
740
- try { document.body.style.overflow = ''; } catch (e) { }
741
- try { document.documentElement.style.overflow = ''; } catch (e) { }
742
- }
743
-
744
- // Restore previous focus if possible
745
- try {
746
- if (this._lastActiveElement && typeof this._lastActiveElement.focus === 'function') {
747
- this._lastActiveElement.focus();
748
- }
749
- } catch (e) { }
750
- this._lastActiveElement = null;
751
-
752
- if (callback) callback();
753
- resolve();
754
- }
755
- });
756
- });
757
- }
758
-
759
- /**
760
- * Métodos de acceso rápido
761
- */
762
- success(message, title = null, options = {}) {
763
- this.show({
764
- type: 'success',
765
- title: title || this.getDefaultTitle('success'),
766
- message,
767
- ...options
768
- });
769
- }
770
-
771
- error(message, title = null, options = {}) {
772
- this.show({
773
- type: 'error',
774
- title: title || this.getDefaultTitle('error'),
775
- message,
776
- ...options
777
- });
778
- }
779
-
780
- warning(message, title = null, options = {}) {
781
- this.show({
782
- type: 'warning',
783
- title: title || this.getDefaultTitle('warning'),
784
- message,
785
- ...options
786
- });
787
- }
788
-
789
- info(message, title = null, options = {}) {
790
- this.show({
791
- type: 'info',
792
- title: title || this.getDefaultTitle('info'),
793
- message,
794
- ...options
795
- });
796
- }
797
-
798
- /**
799
- * Mostrar notificación de carga con spinner
800
- * Útil para operaciones async, cargas desde backend, etc
801
- *
802
- * @param {string} message - Mensaje a mostrar
803
- * @param {string} title - Título (opcional, default: 'Cargando...')
804
- * @param {Object} options - Opciones adicionales
805
- * @param {number} options.timer - Auto-cerrar después de X ms (opcional)
806
- * @param {boolean} options.allowOutsideClick - Permitir cerrar haciendo click fuera (default: false)
807
- * @param {boolean} options.allowEscapeKey - Permitir cerrar con tecla ESC (default: false)
808
- * @param {Function} options.onClose - Callback al cerrar (opcional)
809
- * @returns {Promise} Promesa que se resuelve cuando se cierre la notificación
810
- *
811
- * @example
812
- * // Usar como promise
813
- * notify.loading('Procesando...', 'Espera')
814
- * .then(() => console.log('Completado'));
815
- *
816
- * // Cerrar manualmente
817
- * const loadingPromise = notify.loading('Subiendo archivo...');
818
- * setTimeout(() => notify.closeLoading(), 3000);
819
- *
820
- * // Con respuesta de backend
821
- * notify.loading('Obteniendo datos...');
822
- * fetch('/api/datos')
823
- * .then(res => res.json())
824
- * .then(data => {
825
- * notify.closeLoading();
826
- * notify.success('Datos cargados');
827
- * })
828
- * .catch(err => {
829
- * notify.closeLoading();
830
- * notify.error('Error: ' + err.message);
831
- * });
832
- */
833
- loading(message = 'Cargando...', title = 'Espera', options = {}) {
834
- // No cierra al hacer click fuera ni ESC por defecto
835
- const loadingOptions = {
836
- type: 'info',
837
- title,
838
- message,
839
- hideButton: true,
840
- allowOutsideClick: false,
841
- allowEscapeKey: false,
842
- ...options
843
- };
844
-
845
- // Crear una promesa wrapper para poder guardar la referencia y resolver después
846
- const loadingPromise = this.show(loadingOptions);
847
-
848
- // Guardar referencia a esta promesa para poder cerrarla después
849
- this._currentLoadingPromise = loadingPromise;
850
-
851
- return loadingPromise;
852
- }
853
-
854
- /**
855
- * Cerrar la notificación de carga actual
856
- *
857
- * @param {Function} callback - Callback al cerrar (opcional)
858
- * @returns {Promise} Promesa que se resuelve cuando se cierre
859
- *
860
- * @example
861
- * notify.closeLoading();
862
- * // o con callback
863
- * notify.closeLoading(() => console.log('Loading cerrado'));
864
- */
865
- closeLoading(callback = null) {
866
- this._currentLoadingPromise = null;
867
- return this.close(callback);
868
- }
869
-
870
- /**
871
- * Alias para cerrar/ocultar la notificación actual
872
- */
873
- hide(callback = null) {
874
- return this.close(callback);
875
- }
876
-
877
- /**
878
- * Compatibilidad con nombre mal escrito 'hiden'
879
- */
880
- hiden(callback = null) {
881
- return this.close(callback);
882
- }
883
-
884
- /**
885
- * Formatea segundos a mm:ss
886
- */
887
- _formatTime(seconds) {
888
- const s = Math.max(0, Math.floor(seconds));
889
- const mm = Math.floor(s / 60).toString().padStart(2, '0');
890
- const ss = (s % 60).toString().padStart(2, '0');
891
- return `${mm}:${ss}`;
892
- }
893
-
894
- }
895
-
896
- // Crear instancia global
897
- window.notify = new NotificationSystem();
898
-
899
- // Exponer también como objeto global para compatibilidad
900
- window.Notification = window.notify;
901
- }
902
- })();
327
+ `;
328
+ document.head.appendChild(style);
329
+ }
330
+ getIcon(type) {
331
+ const icons = {
332
+ 'success': '<i class="bx bx-check" aria-hidden="true"></i>',
333
+ 'error': '<i class="bx bx-x" aria-hidden="true"></i>',
334
+ 'warning': '<i class="bx bx-error" aria-hidden="true"></i>',
335
+ 'info': '<i class="bx bx-info-circle" aria-hidden="true"></i>',
336
+ 'question': '<i class="bx bx-question-mark" aria-hidden="true"></i>'
337
+ };
338
+ return icons[type] || icons.info;
339
+ }
340
+ getDefaultTitle(type) {
341
+ const titles = {
342
+ 'success': '¡Éxito!',
343
+ 'error': 'Error',
344
+ 'warning': 'Advertencia',
345
+ 'info': 'Información',
346
+ 'question': 'Pregunta'
347
+ };
348
+ return titles[type] || 'Notificación';
349
+ }
350
+ getButtonGradient(type) {
351
+ const gradients = {
352
+ 'success': 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
353
+ 'error': 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
354
+ 'warning': 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
355
+ 'info': 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
356
+ 'question': 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'
357
+ };
358
+ return gradients[type] || gradients.info;
359
+ }
360
+ getButtonShadow(type) {
361
+ const shadows = {
362
+ 'success': 'rgba(16, 185, 129, 0)',
363
+ 'error': 'rgba(239, 68, 68, 0)',
364
+ 'warning': 'rgba(245, 159, 11, 0)',
365
+ 'info': 'rgba(59, 131, 246, 0)',
366
+ 'question': 'rgba(108, 99, 245, 0)'
367
+ };
368
+ return shadows[type] || shadows.info;
369
+ }
370
+ show(options = {}) {
371
+ // Cerrar notificación existente si hay (esperar a que termine)
372
+ if (this.currentNotification) {
373
+ const oldOverlay = this.currentNotification;
374
+ this.currentNotification = null;
375
+ try {
376
+ if (oldOverlay && oldOverlay.parentNode) {
377
+ oldOverlay.parentNode.removeChild(oldOverlay);
378
+ }
379
+ }
380
+ catch (e) { }
381
+ }
382
+ const { type = 'info', title = this.getDefaultTitle(type), message = '', buttonText = 'OK', buttonColor = null, onClose = null, timer = null, allowOutsideClick = true, allowEscapeKey = true, hideButton = false, buttons = null } = options;
383
+ const showCloseButton = options.showCloseButton === true;
384
+ try {
385
+ document.body.style.overflow = 'hidden';
386
+ }
387
+ catch (e) { }
388
+ try {
389
+ document.documentElement.style.overflow = 'hidden';
390
+ }
391
+ catch (e) { }
392
+ const overlay = document.createElement('div');
393
+ overlay.className = 'notification-overlay';
394
+ overlay.tabIndex = -1;
395
+ overlay.setAttribute('role', 'dialog');
396
+ overlay.setAttribute('aria-modal', 'true');
397
+ overlay.style.pointerEvents = 'auto';
398
+ const box = document.createElement('div');
399
+ box.className = 'notification-box';
400
+ const icon = document.createElement('div');
401
+ icon.className = `notification-icon ${type}`;
402
+ if (hideButton && type === 'info') {
403
+ icon.className = 'notification-loading-container';
404
+ icon.innerHTML = '<div class="notification-spinner"></div>';
405
+ icon.style.background = 'transparent';
406
+ icon.style.boxShadow = 'none';
407
+ icon.style.width = '100px';
408
+ icon.style.height = '100px';
409
+ }
410
+ else {
411
+ icon.innerHTML = this.getIcon(type);
412
+ }
413
+ const titleElement = document.createElement('h3');
414
+ titleElement.className = 'notification-title';
415
+ titleElement.textContent = title;
416
+ const messageElement = document.createElement('p');
417
+ messageElement.className = 'notification-message';
418
+ messageElement.textContent = message;
419
+ let customContent = null;
420
+ if (options.html || options.content) {
421
+ customContent = document.createElement('div');
422
+ customContent.className = 'notification-content';
423
+ if (options.html) {
424
+ try {
425
+ customContent.innerHTML = options.html;
426
+ }
427
+ catch (e) {
428
+ customContent.textContent = options.html;
429
+ }
430
+ }
431
+ else if (options.content && options.content instanceof HTMLElement) {
432
+ customContent.appendChild(options.content);
433
+ }
434
+ }
435
+ const closeHandler = () => {
436
+ return this.close(onClose);
437
+ };
438
+ let button = null;
439
+ let buttonContainer = null;
440
+ if (!hideButton) {
441
+ if (Array.isArray(buttons) && buttons.length) {
442
+ buttonContainer = document.createElement('div');
443
+ buttonContainer.className = 'notification-button-group';
444
+ buttons.forEach((btn) => {
445
+ const btnEl = document.createElement('button');
446
+ btnEl.className = 'notification-button';
447
+ btnEl.textContent = btn.text || 'OK';
448
+ const finalBtnColor = btn.color || this.getButtonGradient(type);
449
+ const btnShadow = btn.shadowColor || this.getButtonShadow(type);
450
+ btnEl.style.background = finalBtnColor;
451
+ btnEl.style.boxShadow = `0 4px 12px ${btnShadow}`;
452
+ btnEl.addEventListener('click', (e) => {
453
+ e.stopPropagation();
454
+ e.preventDefault();
455
+ try {
456
+ closeHandler().then(() => {
457
+ if (typeof btn.onClick === 'function') {
458
+ try {
459
+ const res = btn.onClick();
460
+ if (res && typeof res.then === 'function') {
461
+ res.catch((err) => console.error(err));
462
+ }
463
+ }
464
+ catch (err) {
465
+ console.error(err);
466
+ }
467
+ }
468
+ }).catch(() => { });
469
+ }
470
+ catch (err) {
471
+ console.error(err);
472
+ }
473
+ });
474
+ btnEl.addEventListener('mouseenter', () => {
475
+ btnEl.style.boxShadow = `0 6px 16px ${btnShadow}`;
476
+ });
477
+ btnEl.addEventListener('mouseleave', () => {
478
+ btnEl.style.boxShadow = `0 4px 12px ${btnShadow}`;
479
+ });
480
+ buttonContainer.appendChild(btnEl);
481
+ });
482
+ }
483
+ else if (options.onConfirm || options.onCancel || options.confirmText || options.cancelText) {
484
+ buttonContainer = document.createElement('div');
485
+ buttonContainer.className = 'notification-button-group';
486
+ const cancelText = options.cancelText || 'Cancelar';
487
+ const confirmText = options.confirmText || 'Aceptar';
488
+ const cancelBtn = document.createElement('button');
489
+ cancelBtn.className = 'notification-button';
490
+ cancelBtn.textContent = cancelText;
491
+ const cancelColor = options.cancelColor || 'linear-gradient(135deg, #9ca3af 0%, #6b7280 100%)';
492
+ const cancelShadow = options.cancelShadow || 'rgba(107,114,128,0.25)';
493
+ cancelBtn.style.background = cancelColor;
494
+ cancelBtn.style.boxShadow = `0 4px 12px ${cancelShadow}`;
495
+ cancelBtn.addEventListener('click', (e) => {
496
+ e.stopPropagation();
497
+ e.preventDefault();
498
+ closeHandler().then(() => {
499
+ try {
500
+ if (typeof options.onCancel === 'function') {
501
+ const res = options.onCancel();
502
+ if (res && typeof res.then === 'function') {
503
+ res.catch((err) => console.error(err));
504
+ }
505
+ }
506
+ }
507
+ catch (err) {
508
+ console.error(err);
509
+ }
510
+ }).catch(() => { });
511
+ });
512
+ cancelBtn.addEventListener('mouseenter', () => { cancelBtn.style.boxShadow = `0 6px 16px ${cancelShadow}`; });
513
+ cancelBtn.addEventListener('mouseleave', () => { cancelBtn.style.boxShadow = `0 4px 12px ${cancelShadow}`; });
514
+ const confirmBtn = document.createElement('button');
515
+ confirmBtn.className = 'notification-button';
516
+ confirmBtn.textContent = confirmText;
517
+ const confirmColor = options.confirmColor || this.getButtonGradient(type);
518
+ const confirmShadow = options.confirmShadow || this.getButtonShadow(type);
519
+ confirmBtn.style.background = confirmColor;
520
+ confirmBtn.style.boxShadow = `0 4px 12px ${confirmShadow}`;
521
+ confirmBtn.addEventListener('click', async (e) => {
522
+ e.stopPropagation();
523
+ e.preventDefault();
524
+ try {
525
+ await closeHandler();
526
+ if (typeof options.onConfirm === 'function') {
527
+ const res = options.onConfirm();
528
+ if (res && typeof res.then === 'function') {
529
+ await res;
530
+ }
531
+ }
532
+ }
533
+ catch (err) {
534
+ console.error(err);
535
+ }
536
+ });
537
+ confirmBtn.addEventListener('mouseenter', () => { confirmBtn.style.boxShadow = `0 6px 16px ${confirmShadow}`; });
538
+ confirmBtn.addEventListener('mouseleave', () => { confirmBtn.style.boxShadow = `0 4px 12px ${confirmShadow}`; });
539
+ buttonContainer.appendChild(cancelBtn);
540
+ buttonContainer.appendChild(confirmBtn);
541
+ }
542
+ else if (buttonText) {
543
+ button = document.createElement('button');
544
+ button.className = 'notification-button';
545
+ button.textContent = buttonText;
546
+ const finalButtonColor = buttonColor || this.getButtonGradient(type);
547
+ const buttonShadowColor = this.getButtonShadow(type);
548
+ button.style.background = finalButtonColor;
549
+ button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
550
+ }
551
+ }
552
+ let closeBtn = null;
553
+ if (showCloseButton) {
554
+ closeBtn = document.createElement('button');
555
+ closeBtn.setAttribute('aria-label', 'Cerrar');
556
+ closeBtn.className = 'notification-close';
557
+ closeBtn.innerHTML = '&times;';
558
+ closeBtn.addEventListener('click', (e) => {
559
+ e.stopPropagation();
560
+ closeHandler();
561
+ });
562
+ }
563
+ box.appendChild(icon);
564
+ if (customContent) {
565
+ const descId = 'notify-desc-' + Date.now();
566
+ customContent.id = descId;
567
+ overlay.setAttribute('aria-describedby', descId);
568
+ box.appendChild(customContent);
569
+ }
570
+ else {
571
+ box.appendChild(titleElement);
572
+ box.appendChild(messageElement);
573
+ }
574
+ if (closeBtn)
575
+ box.appendChild(closeBtn);
576
+ if (buttonContainer) {
577
+ box.appendChild(buttonContainer);
578
+ }
579
+ else if (button) {
580
+ box.appendChild(button);
581
+ }
582
+ overlay.appendChild(box);
583
+ document.body.appendChild(overlay);
584
+ const overlayMeta = overlay;
585
+ const closePromise = new Promise((resolveClose) => {
586
+ try {
587
+ overlayMeta._externalResolve = resolveClose;
588
+ }
589
+ catch (e) { }
590
+ });
591
+ try {
592
+ const live = document.getElementById('notify-live');
593
+ if (live) {
594
+ live.textContent = `${title}: ${message}`;
595
+ }
596
+ }
597
+ catch (e) { }
598
+ try {
599
+ this._lastActiveElement = document.activeElement;
600
+ }
601
+ catch (e) {
602
+ this._lastActiveElement = null;
603
+ }
604
+ this.currentNotification = overlay;
605
+ try {
606
+ const focusable = box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])');
607
+ if (focusable && focusable.length) {
608
+ focusable[0].focus();
609
+ }
610
+ else if (button) {
611
+ button.focus();
612
+ }
613
+ else {
614
+ overlay.focus();
615
+ }
616
+ }
617
+ catch (e) {
618
+ try {
619
+ overlay.focus();
620
+ }
621
+ catch (err) { }
622
+ }
623
+ const focusTrap = (e) => {
624
+ if (e.key !== 'Tab')
625
+ return;
626
+ const focusableNodes = Array.from(box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'));
627
+ const focusable = focusableNodes.filter((el) => el instanceof HTMLElement && el.offsetParent !== null);
628
+ if (!focusable.length) {
629
+ e.preventDefault();
630
+ return;
631
+ }
632
+ const first = focusable[0];
633
+ const last = focusable[focusable.length - 1];
634
+ if (!e.shiftKey && document.activeElement === last) {
635
+ e.preventDefault();
636
+ first.focus();
637
+ }
638
+ else if (e.shiftKey && document.activeElement === first) {
639
+ e.preventDefault();
640
+ last.focus();
641
+ }
642
+ };
643
+ overlayMeta._focusTrap = focusTrap;
644
+ document.addEventListener('keydown', focusTrap);
645
+ const anim = options.anim || {};
646
+ const overlayDuration = typeof anim.overlayDuration === 'number' ? anim.overlayDuration : 150;
647
+ const overlayEasing = anim.overlayEasing || 'easeOutQuad';
648
+ const boxDuration = typeof anim.boxDuration === 'number' ? anim.boxDuration : 200;
649
+ const boxDelay = typeof anim.boxDelay === 'number' ? anim.boxDelay : 50;
650
+ const boxEasing = anim.boxEasing || 'easeOutBack';
651
+ const boxStartScale = typeof anim.boxStartScale === 'number' ? anim.boxStartScale : 0.8;
652
+ const iconDuration = typeof anim.iconDuration === 'number' ? anim.iconDuration : 250;
653
+ const iconDelay = typeof anim.iconDelay === 'number' ? anim.iconDelay : 100;
654
+ const iconRotate = (typeof anim.iconRotate === 'number') ? anim.iconRotate : (type === 'success' ? -90 : type === 'error' ? 90 : 0);
655
+ if (typeof anim.overlayOpacity === 'number') {
656
+ overlay.style.backgroundColor = `rgba(0,0,0,${anim.overlayOpacity})`;
657
+ }
658
+ anime({
659
+ targets: overlay,
660
+ opacity: [0, 1],
661
+ duration: overlayDuration,
662
+ easing: overlayEasing
663
+ });
664
+ anime({
665
+ targets: box,
666
+ scale: [boxStartScale, 1],
667
+ opacity: [0, 1],
668
+ duration: boxDuration,
669
+ easing: boxEasing,
670
+ delay: boxDelay
671
+ });
672
+ anime({
673
+ targets: icon,
674
+ scale: [0, 1],
675
+ rotate: [iconRotate, 0],
676
+ duration: iconDuration,
677
+ easing: boxEasing,
678
+ delay: iconDelay
679
+ });
680
+ if (button) {
681
+ const buttonShadowColor = this.getButtonShadow(type);
682
+ button.addEventListener('mouseenter', () => {
683
+ button.style.boxShadow = `0 6px 16px ${buttonShadowColor}`;
684
+ });
685
+ button.addEventListener('mouseleave', () => {
686
+ button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
687
+ });
688
+ button.addEventListener('click', (e) => {
689
+ e.stopPropagation();
690
+ e.preventDefault();
691
+ closeHandler().catch(() => { });
692
+ });
693
+ }
694
+ if (allowOutsideClick) {
695
+ overlay.addEventListener('click', (e) => {
696
+ if (!box.contains(e.target)) {
697
+ closeHandler();
698
+ }
699
+ });
700
+ }
701
+ if (timer) {
702
+ setTimeout(() => {
703
+ closeHandler();
704
+ }, timer);
705
+ }
706
+ if (allowEscapeKey) {
707
+ const escHandler = (e) => {
708
+ if (e.key === 'Escape') {
709
+ closeHandler();
710
+ document.removeEventListener('keydown', escHandler);
711
+ }
712
+ };
713
+ overlayMeta._escHandler = escHandler;
714
+ document.addEventListener('keydown', escHandler);
715
+ }
716
+ return closePromise;
717
+ }
718
+ close(callback = null) {
719
+ if (!this.currentNotification) {
720
+ return Promise.resolve();
721
+ }
722
+ const overlay = this.currentNotification;
723
+ const overlayMeta = overlay;
724
+ const box = overlay.querySelector('.notification-box');
725
+ this.currentNotification = null;
726
+ anime({
727
+ targets: box,
728
+ scale: 0.8,
729
+ opacity: 0,
730
+ duration: 100,
731
+ easing: 'easeInQuad'
732
+ });
733
+ return new Promise((resolve) => {
734
+ anime({
735
+ targets: overlay,
736
+ opacity: 0,
737
+ duration: 100,
738
+ easing: 'easeInQuad',
739
+ complete: () => {
740
+ try {
741
+ if (overlayMeta && overlayMeta._escHandler) {
742
+ document.removeEventListener('keydown', overlayMeta._escHandler);
743
+ overlayMeta._escHandler = undefined;
744
+ }
745
+ }
746
+ catch (e) { }
747
+ try {
748
+ if (overlayMeta && overlayMeta._focusTrap) {
749
+ document.removeEventListener('keydown', overlayMeta._focusTrap);
750
+ overlayMeta._focusTrap = undefined;
751
+ }
752
+ }
753
+ catch (e) { }
754
+ try {
755
+ if (overlayMeta && typeof overlayMeta._externalResolve === 'function') {
756
+ try {
757
+ overlayMeta._externalResolve();
758
+ }
759
+ catch (er) { }
760
+ overlayMeta._externalResolve = undefined;
761
+ }
762
+ }
763
+ catch (e) { }
764
+ try {
765
+ if (overlay && overlay.parentNode) {
766
+ overlay.parentNode.removeChild(overlay);
767
+ }
768
+ }
769
+ catch (e) {
770
+ try {
771
+ overlay.remove();
772
+ }
773
+ catch (er) { }
774
+ }
775
+ if (!this.currentNotification) {
776
+ try {
777
+ document.body.style.overflow = '';
778
+ }
779
+ catch (e) { }
780
+ try {
781
+ document.documentElement.style.overflow = '';
782
+ }
783
+ catch (e) { }
784
+ }
785
+ try {
786
+ if (this._lastActiveElement && typeof this._lastActiveElement.focus === 'function') {
787
+ this._lastActiveElement.focus();
788
+ }
789
+ }
790
+ catch (e) { }
791
+ this._lastActiveElement = null;
792
+ if (callback)
793
+ callback();
794
+ resolve();
795
+ }
796
+ });
797
+ });
798
+ }
799
+ success(message, title = null, options = {}) {
800
+ this.show(Object.assign({ type: 'success', title: title || this.getDefaultTitle('success'), message }, options));
801
+ }
802
+ error(message, title = null, options = {}) {
803
+ this.show(Object.assign({ type: 'error', title: title || this.getDefaultTitle('error'), message }, options));
804
+ }
805
+ warning(message, title = null, options = {}) {
806
+ this.show(Object.assign({ type: 'warning', title: title || this.getDefaultTitle('warning'), message }, options));
807
+ }
808
+ question(message, title = null, options = {}) {
809
+ this.show(Object.assign({ type: 'question', title: title || this.getDefaultTitle('question'), message }, options));
810
+ }
811
+ info(message, title = null, options = {}) {
812
+ this.show(Object.assign({ type: 'info', title: title || this.getDefaultTitle('info'), message }, options));
813
+ }
814
+ loading(message = 'Cargando...', title = 'Espera', options = {}) {
815
+ const loadingOptions = Object.assign({ type: 'info', title,
816
+ message, hideButton: true, allowOutsideClick: false, allowEscapeKey: false }, options);
817
+ const loadingPromise = this.show(loadingOptions);
818
+ this._currentLoadingPromise = loadingPromise;
819
+ return loadingPromise;
820
+ }
821
+ closeLoading(callback = null) {
822
+ this._currentLoadingPromise = null;
823
+ return this.close(callback);
824
+ }
825
+ hide(callback = null) { return this.close(callback); }
826
+ hiden(callback = null) { return this.close(callback); }
827
+ _formatTime(seconds) {
828
+ const s = Math.max(0, Math.floor(seconds));
829
+ const mm = Math.floor(s / 60).toString().padStart(2, '0');
830
+ const ss = (s % 60).toString().padStart(2, '0');
831
+ return `${mm}:${ss}`;
832
+ }
833
+ }
834
+ const notifyInstance = new NotificationSystem();
835
+ const w = window;
836
+ w.notify = notifyInstance;
837
+ w.Notification = notifyInstance;
838
+ }
839
+ })();
840
+ //# sourceMappingURL=notification-system.js.map