/** * UI Utilities * Reusable UI components and helpers */ import CONFIG from '../config.js'; /** * Modal Manager * Simplifies modal open/close operations */ export class ModalManager { /** * Show a modal * @param {string} modalId - ID of modal element * @param {Function} onOpen - Optional callback when modal opens */ static show(modalId, onOpen = null) { const modal = document.getElementById(modalId); if (!modal) { console.error(`Modal #${modalId} not found`); return; } modal.classList.remove('hidden'); if (onOpen) { onOpen(modal); } } /** * Hide a modal * @param {string} modalId - ID of modal element * @param {Function} onClose - Optional callback when modal closes */ static hide(modalId, onClose = null) { const modal = document.getElementById(modalId); if (!modal) { console.error(`Modal #${modalId} not found`); return; } modal.classList.add('hidden'); if (onClose) { onClose(modal); } } /** * Setup modal with close handlers * @param {string} modalId - ID of modal element * @param {string} closeButtonId - ID of close button * @param {Function} onClose - Optional cleanup callback * @returns {Function} Cleanup function */ static setup(modalId, closeButtonId, onClose = null) { const modal = document.getElementById(modalId); const closeBtn = document.getElementById(closeButtonId); if (!modal || !closeBtn) { console.error(`Modal #${modalId} or close button #${closeButtonId} not found`); return () => {}; } const handleClose = () => { this.hide(modalId, onClose); }; // Close on button click closeBtn.addEventListener('click', handleClose); // Close on outside click modal.addEventListener('click', (e) => { if (e.target === modal) { handleClose(); } }); // Close on ESC key const handleEscape = (e) => { if (e.key === 'Escape') { handleClose(); } }; document.addEventListener('keydown', handleEscape); // Return cleanup function return () => { closeBtn.removeEventListener('click', handleClose); document.removeEventListener('keydown', handleEscape); }; } } /** * Toast Notification System * Better than window.alert() */ export class Toast { static container = null; /** * Initialize toast container */ static init() { if (this.container) return; this.container = document.createElement('div'); this.container.id = 'toast-container'; this.container.style.cssText = ` position: fixed; top: ${CONFIG.UI.TOOLBAR_HEIGHT + 20}px; right: 20px; z-index: ${CONFIG.COLORS.PRIMARY}; display: flex; flex-direction: column; gap: 10px; pointer-events: none; `; document.body.appendChild(this.container); } /** * Show a toast notification * @param {string} message - Message to display * @param {string} type - 'success', 'error', 'warning', 'info' * @param {number} duration - Display duration in ms (0 = persist) */ static show(message, type = 'info', duration = 3000) { this.init(); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const colors = { success: CONFIG.COLORS.SUCCESS, error: CONFIG.COLORS.DANGER, warning: CONFIG.COLORS.WARNING, info: CONFIG.COLORS.PRIMARY }; toast.style.cssText = ` background-color: ${colors[type] || colors.info}; color: white; padding: 12px 20px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); max-width: 400px; word-wrap: break-word; pointer-events: auto; cursor: pointer; animation: slideIn 0.3s ease; font-size: 14px; `; toast.textContent = message; // Click to dismiss toast.addEventListener('click', () => { this.remove(toast); }); this.container.appendChild(toast); // Auto-remove after duration if (duration > 0) { setTimeout(() => { this.remove(toast); }, duration); } return toast; } /** * Remove a toast */ static remove(toast) { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } // Convenience methods static success(message, duration) { return this.show(message, 'success', duration); } static error(message, duration) { return this.show(message, 'error', duration); } static warning(message, duration) { return this.show(message, 'warning', duration); } static info(message, duration) { return this.show(message, 'info', duration); } } /** * Loading Indicator */ export class LoadingIndicator { static overlay = null; static show(message = 'Loading...') { if (this.overlay) return; this.overlay = document.createElement('div'); this.overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; `; const spinner = document.createElement('div'); spinner.style.cssText = ` background-color: white; padding: 30px 40px; border-radius: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); text-align: center; font-size: 16px; color: #333; `; spinner.innerHTML = `