import { t } from '../i18n.js' import { escapeHTML } from '../utils/helpers.js' /** * Error Boundary Component * Catches errors in child components and displays fallback UI */ class ErrorBoundary extends HTMLElement { constructor() { super() this.hasError = false this.error = null } connectedCallback() { this.setupErrorHandling() } setupErrorHandling() { // Catch errors from child components this.addEventListener('error', (e) => { e.stopPropagation() this.handleError(e.error || e.message || 'Unknown error') }) } handleError(error) { console.error('ErrorBoundary caught:', error) this.hasError = true this.error = error this.renderError() } renderError() { const errorMessage = this.error?.message || String(this.error) || t('common.error') this.innerHTML = /* html */`
⚠️

${t('error.title')}

${escapeHTML(errorMessage)}

` this.querySelector('.error-retry')?.addEventListener('click', () => this.retry()) } retry() { this.hasError = false this.error = null this.innerHTML = '' this.dispatchEvent(new CustomEvent('retry', { bubbles: true })) } reset() { this.hasError = false this.error = null } } customElements.define('error-boundary', ErrorBoundary) /** * Global error handler for uncaught errors */ export function setupGlobalErrorHandler() { window.addEventListener('error', (event) => { console.error('Global error:', event.error) // Don't show UI for script loading errors if (event.filename && event.filename.includes('.js')) { showErrorToast(event.message || t('common.error')) } }) window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled promise rejection:', event.reason) showErrorToast(event.reason?.message || t('common.error')) }) // Offline/Online detection setupOfflineIndicator() } /** * Shows/hides offline indicator based on network status */ function setupOfflineIndicator() { const updateStatus = () => { let indicator = document.querySelector('.offline-indicator') if (!navigator.onLine) { if (!indicator) { indicator = document.createElement('div') indicator.className = 'offline-indicator' indicator.innerHTML = ` 📡 Offline ` document.body.appendChild(indicator) requestAnimationFrame(() => indicator.classList.add('visible')) } } else { if (indicator) { indicator.classList.remove('visible') setTimeout(() => indicator.remove(), 300) } } } window.addEventListener('online', updateStatus) window.addEventListener('offline', updateStatus) // Check on init updateStatus() } /** * Shows a temporary error toast notification */ function showErrorToast(message) { // Remove existing toast document.querySelector('.error-toast')?.remove() const toast = document.createElement('div') toast.className = 'error-toast' toast.innerHTML = /* html */` ⚠️ ${escapeHTML(message)} ` document.body.appendChild(toast) // Auto-dismiss after 5 seconds const timeout = setTimeout(() => toast.remove(), 5000) toast.querySelector('.error-toast-close')?.addEventListener('click', () => { clearTimeout(timeout) toast.remove() }) // Trigger animation requestAnimationFrame(() => toast.classList.add('visible')) } // Styles const style = document.createElement('style') style.textContent = /* css */` error-boundary { display: block; } error-boundary .error-boundary { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: var(--space-2xl); text-align: center; min-height: 200px; } error-boundary .error-icon { font-size: 3rem; margin-bottom: var(--space-md); filter: grayscale(1); } error-boundary .error-title { font-size: var(--font-size-lg); margin: 0 0 var(--space-sm); color: var(--color-text); } error-boundary .error-message { font-size: var(--font-size-sm); color: var(--color-text-muted); margin: 0 0 var(--space-lg); max-width: 400px; } /* Error Toast */ .error-toast { position: fixed; bottom: var(--space-lg); left: 50%; transform: translateX(-50%) translateY(100px); display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm) var(--space-md); background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); z-index: 10000; opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } .error-toast.visible { transform: translateX(-50%) translateY(0); opacity: 1; } .error-toast-icon { filter: grayscale(1); } .error-toast-message { font-size: var(--font-size-sm); color: var(--color-text); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .error-toast-close { background: none; border: none; font-size: var(--font-size-lg); color: var(--color-text-muted); cursor: pointer; padding: 0; line-height: 1; } .error-toast-close:hover { color: var(--color-text); } /* Offline Indicator */ .offline-indicator { position: fixed; top: var(--space-md); left: 50%; transform: translateX(-50%) translateY(-100px); display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) var(--space-md); background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-full); box-shadow: var(--shadow-md); z-index: 10001; opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } .offline-indicator.visible { transform: translateX(-50%) translateY(0); opacity: 1; } .offline-indicator .offline-icon { filter: grayscale(1); } .offline-indicator .offline-text { font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); color: var(--color-text); } ` document.head.appendChild(style) export { ErrorBoundary }