import { createModuleLogger } from "./LoggerUtils.js"; const log = createModuleLogger('NotificationUtils'); /** * Utility functions for showing notifications to the user */ /** * Shows a temporary notification to the user * @param message - The message to show * @param backgroundColor - Background color (default: #4a6cd4) * @param duration - Duration in milliseconds (default: 3000) */ export function showNotification(message, backgroundColor = "#4a6cd4", duration = 3000, type = "info") { // Remove any existing prefix to avoid double prefixing message = message.replace(/^\[Layer Forge\]\s*/, ""); // Type-specific config const config = { success: { icon: "✔️", title: "Success", bg: "#1fd18b" }, error: { icon: "❌", title: "Error", bg: "#ff6f6f" }, info: { icon: "ℹ️", title: "Info", bg: "#4a6cd4" }, warning: { icon: "⚠️", title: "Warning", bg: "#ffd43b" }, alert: { icon: "⚠️", title: "Alert", bg: "#fff7cc" } }[type]; // --- Get or create the main notification container --- let container = document.getElementById('lf-notification-container'); if (!container) { container = document.createElement('div'); container.id = 'lf-notification-container'; container.style.cssText = ` position: fixed; top: 24px; right: 24px; z-index: 10001; display: flex; flex-direction: row-reverse; gap: 16px; align-items: flex-start; `; document.body.appendChild(container); } // --- Dark, modern notification style --- const notification = document.createElement('div'); notification.style.cssText = ` min-width: 380px; max-width: 440px; max-height: 80vh; background: rgba(30, 32, 41, 0.9); color: #fff; border-radius: 12px; box-shadow: 0 4px 32px rgba(0,0,0,0.25); display: flex; flex-direction: column; padding: 0; font-family: 'Segoe UI', 'Arial', sans-serif; overflow: hidden; border: 1px solid rgba(80, 80, 80, 0.5); backdrop-filter: blur(8px); animation: lf-fadein 0.3s ease-out; `; // --- Header (non-scrollable) --- const header = document.createElement('div'); header.style.cssText = `display: flex; align-items: flex-start; padding: 16px 20px; position: relative; flex-shrink: 0;`; const leftBar = document.createElement('div'); leftBar.style.cssText = `position: absolute; left: 0; top: 0; bottom: 0; width: 6px; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; border-radius: 3px 0 0 3px;`; const iconContainer = document.createElement('div'); iconContainer.style.cssText = `width: 48px; height: 48px; min-width: 48px; min-height: 48px; display: flex; align-items: center; justify-content: center; margin-left: 18px; margin-right: 18px;`; iconContainer.innerHTML = { success: ``, error: ``, info: ``, warning: ``, alert: `` }[type]; const headerTextContent = document.createElement('div'); headerTextContent.style.cssText = `display: flex; flex-direction: column; justify-content: center; flex: 1; min-width: 0;`; const titleSpan = document.createElement('div'); titleSpan.style.cssText = `font-weight: 700; font-size: 16px; margin-bottom: 4px; color: #fff; text-transform: uppercase; letter-spacing: 0.5px;`; titleSpan.textContent = config.title; headerTextContent.appendChild(titleSpan); const topRightContainer = document.createElement('div'); topRightContainer.style.cssText = `position: absolute; top: 14px; right: 18px; display: flex; align-items: center; gap: 12px;`; const tag = document.createElement('span'); tag.style.cssText = `font-size: 11px; font-weight: 600; color: #fff; background: ${config.bg}; border-radius: 4px; padding: 2px 8px; box-shadow: 0 0 8px ${config.bg};`; tag.innerHTML = '🎨 Layer Forge'; const getTextColorForBg = (hexColor) => { const r = parseInt(hexColor.slice(1, 3), 16), g = parseInt(hexColor.slice(3, 5), 16), b = parseInt(hexColor.slice(5, 7), 16); return ((0.299 * r + 0.587 * g + 0.114 * b) / 255) > 0.5 ? '#000' : '#fff'; }; tag.style.color = getTextColorForBg(config.bg); const closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.setAttribute("aria-label", "Close notification"); closeBtn.style.cssText = `background: none; border: none; color: #ccc; font-size: 22px; font-weight: bold; cursor: pointer; padding: 0; opacity: 0.7; transition: opacity 0.15s; line-height: 1;`; topRightContainer.appendChild(tag); topRightContainer.appendChild(closeBtn); header.appendChild(iconContainer); header.appendChild(headerTextContent); header.appendChild(topRightContainer); // --- Scrollable Body --- const body = document.createElement('div'); body.style.cssText = `padding: 0px 20px 16px 20px; overflow-y: auto; flex: 1;`; const msgSpan = document.createElement('div'); msgSpan.style.cssText = `font-size: 14px; color: #ccc; line-height: 1.5; white-space: pre-wrap; word-break: break-word;`; msgSpan.textContent = message; body.appendChild(msgSpan); // --- Progress Bar --- const progressBar = document.createElement('div'); progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; animation: lf-progress ${duration / 1000}s linear; flex-shrink: 0;`; // --- Assemble Notification --- notification.appendChild(leftBar); notification.appendChild(header); notification.appendChild(body); if (type === 'error') { const footer = document.createElement('div'); footer.style.cssText = `padding: 0 20px 12px 86px; flex-shrink: 0;`; const copyButton = document.createElement('button'); copyButton.textContent = 'Copy Error'; copyButton.style.cssText = `background: rgba(255, 111, 111, 0.2); border: 1px solid #ff6f6f; color: #ffafaf; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: background 0.2s;`; copyButton.onmouseenter = () => copyButton.style.background = 'rgba(255, 111, 111, 0.3)'; copyButton.onmouseleave = () => copyButton.style.background = 'rgba(255, 111, 111, 0.2)'; copyButton.onclick = () => { navigator.clipboard.writeText(message) .then(() => showSuccessNotification("Error message copied!", 2000)) .catch(err => console.error('Failed to copy error message: ', err)); }; footer.appendChild(copyButton); notification.appendChild(footer); } notification.appendChild(progressBar); // Add to DOM container.appendChild(notification); // --- Keyframes and Timer Logic --- const styleSheet = document.getElementById('lf-notification-styles'); if (!styleSheet) { const newStyleSheet = document.createElement("style"); newStyleSheet.id = 'lf-notification-styles'; newStyleSheet.innerText = ` @keyframes lf-progress { from { transform: scaleX(1); } to { transform: scaleX(0); } } @keyframes lf-progress-rewind { to { transform: scaleX(1); } } @keyframes lf-fadein { from { opacity: 0; transform: scale(0.95) translateX(20px); } to { opacity: 1; transform: scale(1) translateX(0); } } @keyframes lf-fadeout { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } } .notification-scrollbar::-webkit-scrollbar { width: 8px; } .notification-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; } .notification-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 4px; } .notification-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); } `; document.head.appendChild(newStyleSheet); } body.classList.add('notification-scrollbar'); let dismissTimeout = null; const closeNotification = () => { notification.style.animation = 'lf-fadeout 0.3s ease-out forwards'; notification.addEventListener('animationend', () => { if (notification.parentNode) { notification.parentNode.removeChild(notification); if (container && container.children.length === 0) { container.remove(); } } }); }; closeBtn.onclick = closeNotification; const startDismissTimer = () => { dismissTimeout = window.setTimeout(closeNotification, duration); progressBar.style.animation = `lf-progress ${duration / 1000}s linear`; }; const pauseAndRewindTimer = () => { if (dismissTimeout !== null) clearTimeout(dismissTimeout); dismissTimeout = null; const computedStyle = window.getComputedStyle(progressBar); progressBar.style.transform = computedStyle.transform; progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards'; }; notification.addEventListener('mouseenter', pauseAndRewindTimer); notification.addEventListener('mouseleave', startDismissTimer); startDismissTimer(); log.debug(`Notification shown: [Layer Forge] ${message}`); } /** * Shows a success notification */ export function showSuccessNotification(message, duration = 3000) { showNotification(message, undefined, duration, "success"); } /** * Shows an error notification */ export function showErrorNotification(message, duration = 5000) { showNotification(message, undefined, duration, "error"); } /** * Shows an info notification */ export function showInfoNotification(message, duration = 3000) { showNotification(message, undefined, duration, "info"); } /** * Shows a warning notification */ export function showWarningNotification(message, duration = 3000) { showNotification(message, undefined, duration, "warning"); } /** * Shows an alert notification */ export function showAlertNotification(message, duration = 3000) { showNotification(message, undefined, duration, "alert"); } /** * Shows a sequence of all notification types for debugging purposes. */ export function showAllNotificationTypes(message) { const types = ["success", "error", "info", "warning", "alert"]; types.forEach((type, index) => { const notificationMessage = message || `This is a '${type}' notification.`; setTimeout(() => { showNotification(notificationMessage, undefined, 3000, type); }, index * 400); // Stagger the notifications }); }