/**
* UI Manager - Complete UI/UX Control
* Handles all UI interactions, animations, and state management
*/
class UIManager {
constructor() {
this.toasts = [];
this.modals = new Map();
this.loading = new Set();
this.init();
}
init() {
this.createToastContainer();
this.initializeGlobalHandlers();
this.setupAccessibility();
console.log('✅ UI Manager initialized');
}
/**
* Create toast container if not exists
*/
createToastContainer() {
if (!document.getElementById('toast-container')) {
const container = document.createElement('div');
container.id = 'toast-container';
container.setAttribute('aria-live', 'polite');
container.setAttribute('aria-atomic', 'true');
container.style.cssText = `
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.5rem;
`;
document.body.appendChild(container);
}
}
/**
* Show toast notification
*/
showToast(message, type = 'info', duration = 3000) {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
const id = `toast-${Date.now()}-${Math.random()}`;
toast.id = id;
toast.className = `toast ${type}`;
// Icon based on type
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: 'ℹ️'
};
toast.innerHTML = `
${icons[type] || icons.info}
${this.escapeHtml(message)}
`;
container.appendChild(toast);
this.toasts.push(id);
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => this.closeToast(id), duration);
}
return id;
}
/**
* Close specific toast
*/
closeToast(id) {
const toast = document.getElementById(id);
if (toast) {
toast.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => {
toast.remove();
this.toasts = this.toasts.filter(t => t !== id);
}, 300);
}
}
/**
* Show loading state on element
*/
showLoading(elementId, text = 'Loading...') {
const element = document.getElementById(elementId);
if (!element) return;
this.loading.add(elementId);
const originalContent = element.innerHTML;
element.dataset.originalContent = originalContent;
element.innerHTML = `
`;
}
/**
* Hide loading state
*/
hideLoading(elementId, content = null) {
const element = document.getElementById(elementId);
if (!element) return;
this.loading.delete(elementId);
if (content) {
element.innerHTML = content;
} else if (element.dataset.originalContent) {
element.innerHTML = element.dataset.originalContent;
delete element.dataset.originalContent;
}
}
/**
* Create and show modal
*/
showModal(options = {}) {
const {
id = `modal-${Date.now()}`,
title = 'Modal',
content = '',
size = 'md', // sm, md, lg, xl
onClose = null
} = options;
// Check if modal already exists
if (this.modals.has(id)) {
const existing = this.modals.get(id);
existing.modal.classList.add('active');
return id;
}
const modal = document.createElement('div');
modal.id = id;
modal.className = 'modal active';
modal.innerHTML = `
`;
document.body.appendChild(modal);
this.modals.set(id, { modal, onClose });
// Handle Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
this.closeModal(id);
}
};
document.addEventListener('keydown', handleEscape);
modal.dataset.escapeHandler = handleEscape;
return id;
}
/**
* Close modal
*/
closeModal(id) {
const modalData = this.modals.get(id);
if (!modalData) return;
const { modal, onClose } = modalData;
modal.classList.remove('active');
setTimeout(() => {
modal.remove();
this.modals.delete(id);
if (onClose) onClose();
}, 300);
// Remove escape handler
if (modal.dataset.escapeHandler) {
document.removeEventListener('keydown', modal.dataset.escapeHandler);
}
}
/**
* Show confirmation dialog
*/
async confirm(message, title = 'Confirm') {
return new Promise((resolve) => {
const id = this.showModal({
title,
content: `
${this.escapeHtml(message)}
`,
onClose: () => resolve(false)
});
window.uiManagerResolve = resolve;
});
}
/**
* Show error message
*/
showError(message, details = null) {
const content = `
⚠️ Error
${this.escapeHtml(message)}
${details ? `
${this.escapeHtml(details)}` : ''}
`;
this.showModal({
title: 'Error',
content,
size: 'md'
});
this.showToast(message, 'error');
}
/**
* Initialize global event handlers
*/
initializeGlobalHandlers() {
// Handle all button clicks for better UX
document.addEventListener('click', (e) => {
const button = e.target.closest('button, .btn');
if (button && !button.classList.contains('unstyled')) {
// Add ripple effect
this.createRipple(e, button);
}
});
// Handle form submissions
document.addEventListener('submit', (e) => {
const form = e.target;
if (form.tagName === 'FORM' && !form.classList.contains('no-prevent')) {
// Could add form validation here
}
});
// Handle loading states for async operations
window.addEventListener('beforeunload', (e) => {
if (this.loading.size > 0) {
e.preventDefault();
e.returnValue = 'Operations in progress...';
}
});
}
/**
* Create ripple effect on button click
*/
createRipple(event, button) {
const circle = document.createElement('span');
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
const rect = button.getBoundingClientRect();
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - rect.left - radius}px`;
circle.style.top = `${event.clientY - rect.top - radius}px`;
circle.classList.add('ripple');
const ripple = button.getElementsByClassName('ripple')[0];
if (ripple) {
ripple.remove();
}
circle.style.cssText += `
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: scale(0);
animation: ripple 0.6s ease-out;
pointer-events: none;
`;
button.style.position = 'relative';
button.style.overflow = 'hidden';
button.appendChild(circle);
setTimeout(() => circle.remove(), 600);
}
/**
* Setup accessibility features
*/
setupAccessibility() {
// Add keyboard navigation for modals
document.addEventListener('keydown', (e) => {
// Tab trapping for modals
if (e.key === 'Tab' && this.modals.size > 0) {
// Get active modal
const activeModal = Array.from(this.modals.values())
.map(m => m.modal)
.find(m => m.classList.contains('active'));
if (activeModal) {
const focusableElements = activeModal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
});
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Animate element entrance
*/
animateIn(element, animation = 'fadeIn') {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (!element) return;
element.style.animation = `${animation} 0.3s ease-out`;
}
/**
* Smooth scroll to element
*/
scrollTo(elementId, offset = 0) {
const element = document.getElementById(elementId);
if (!element) return;
const top = element.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top,
behavior: 'smooth'
});
}
/**
* Copy text to clipboard
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showToast('Copied to clipboard!', 'success', 2000);
return true;
} catch (err) {
this.showToast('Failed to copy', 'error');
return false;
}
}
/**
* Format number with locale
*/
formatNumber(number, decimals = 2) {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(number);
}
/**
* Format currency
*/
formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}
/**
* Format relative time
*/
formatRelativeTime(timestamp) {
const now = Date.now();
const diff = now - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return new Date(timestamp).toLocaleDateString();
}
}
// Create global instance
const uiManager = new UIManager();
// Export for use in modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { UIManager, uiManager };
}
// Make available globally
window.uiManager = uiManager;
window.UIManager = UIManager;
// Add CSS for ripple animation
const style = document.createElement('style');
style.textContent = `
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideOutRight {
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
console.log('✅ UI Manager loaded and ready');