diff --git a/js/CanvasView.js b/js/CanvasView.js index d807b2e..ea3aeab 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -8,7 +8,7 @@ import { clearAllCanvasStates } from "./db.js"; import { ImageCache } from "./ImageCache.js"; import { createCanvas } from "./utils/CommonUtils.js"; import { createModuleLogger } from "./utils/LoggerUtils.js"; -import { showErrorNotification, showSuccessNotification } from "./utils/NotificationUtils.js"; +import { showErrorNotification, showSuccessNotification, showInfoNotification } from "./utils/NotificationUtils.js"; import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js"; import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js"; const log = createModuleLogger('Canvas_view'); @@ -312,9 +312,11 @@ async function createCanvasWidget(node, widget, app) { const spinner = $el("div.matting-spinner"); button.appendChild(spinner); button.classList.add('loading'); + showInfoNotification("Starting background removal process...", 2000); try { - if (canvas.canvasSelection.selectedLayers.length !== 1) + if (canvas.canvasSelection.selectedLayers.length !== 1) { throw new Error("Please select exactly one image layer for matting."); + } const selectedLayer = canvas.canvasSelection.selectedLayers[0]; const selectedLayerIndex = canvas.layers.indexOf(selectedLayer); const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer); @@ -327,7 +329,7 @@ async function createCanvasWidget(node, widget, app) { if (!response.ok) { let errorMsg = `Server error: ${response.status} - ${response.statusText}`; if (result && result.error) { - errorMsg = `Error: ${result.error}\n\nDetails: ${result.details}`; + errorMsg = `Error: ${result.error}. Details: ${result.details || 'Check console'}`; } throw new Error(errorMsg); } @@ -340,12 +342,12 @@ async function createCanvasWidget(node, widget, app) { canvas.canvasSelection.updateSelection([newLayer]); canvas.render(); canvas.saveState(); + showSuccessNotification("Background removed successfully!"); } catch (error) { log.error("Matting error:", error); const errorMessage = error.message || "An unknown error occurred."; - const errorDetails = error.stack || (error.details ? JSON.stringify(error.details, null, 2) : "No details available."); - showErrorDialog(errorMessage, errorDetails); + showErrorNotification(`Matting Failed: ${errorMessage}`); } finally { button.classList.remove('loading'); @@ -862,55 +864,6 @@ async function createCanvasWidget(node, widget, app) { panel: controlPanel }; } -function showErrorDialog(message, details) { - const dialog = $el("div.painter-dialog.error-dialog", { - style: { - position: 'fixed', - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)', - zIndex: '9999', - padding: '20px', - background: '#282828', - border: '1px solid #ff4444', - borderRadius: '8px', - minWidth: '400px', - maxWidth: '80vw', - } - }, [ - $el("h3", { textContent: "Matting Error", style: { color: "#ff4444", marginTop: "0" } }), - $el("p", { textContent: message, style: { color: "white" } }), - $el("pre.error-details", { - textContent: details, - style: { - background: "#1e1e1e", - border: "1px solid #444", - padding: "10px", - maxHeight: "300px", - overflowY: "auto", - whiteSpace: "pre-wrap", - wordBreak: "break-all", - color: "#ccc" - } - }), - $el("div.dialog-buttons", { style: { textAlign: "right", marginTop: "20px" } }, [ - $el("button", { - textContent: "Copy Details", - onclick: () => { - navigator.clipboard.writeText(details) - .then(() => showSuccessNotification("Error details copied to clipboard!")) - .catch(err => showErrorNotification("Failed to copy details: " + err)); - } - }), - $el("button", { - textContent: "Close", - style: { marginLeft: "10px" }, - onclick: () => document.body.removeChild(dialog) - }) - ]) - ]); - document.body.appendChild(dialog); -} const canvasNodeInstances = new Map(); app.registerExtension({ name: "Comfy.CanvasNode", diff --git a/js/utils/NotificationUtils.js b/js/utils/NotificationUtils.js index 7172010..1b3589f 100644 --- a/js/utils/NotificationUtils.js +++ b/js/utils/NotificationUtils.js @@ -14,43 +14,32 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration message = message.replace(/^\[Layer Forge\]\s*/, ""); // Type-specific config const config = { - success: { - icon: "✔️", - title: "Success", - bg: "#1fd18b", - color: "#155c3b" - }, - error: { - icon: "❌", - title: "Error", - bg: "#ff6f6f", - color: "#7a2323" - }, - info: { - icon: "ℹ️", - title: "Info", - bg: "#4a6cd4", - color: "#fff" - }, - warning: { - icon: "⚠️", - title: "Warning", - bg: "#ffd43b", - color: "#7a5c00" - }, - alert: { - icon: "⚠️", - title: "Alert", - bg: "#fff7cc", - color: "#7a5c00" - } + 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]; - // --- Dark, modern notification style with sticky header --- + // --- 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 = ` - position: fixed; - top: 24px; - right: 24px; min-width: 380px; max-width: 440px; max-height: 80vh; @@ -58,43 +47,22 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration color: #fff; border-radius: 12px; box-shadow: 0 4px 32px rgba(0,0,0,0.25); - z-index: 10001; - font-size: 14px; display: flex; flex-direction: column; padding: 0; - margin-bottom: 18px; 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.2s; + 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; - `; + 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; - `; + 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.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: ``, @@ -122,8 +90,6 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration 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;`; - closeBtn.onclick = () => { if (notification.parentNode) - notification.parentNode.removeChild(notification); }; topRightContainer.appendChild(tag); topRightContainer.appendChild(closeBtn); header.appendChild(iconContainer); @@ -131,52 +97,70 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration header.appendChild(topRightContainer); // --- Scrollable Body --- const body = document.createElement('div'); - body.style.cssText = ` - padding: 0px 20px 16px 20px; /* Adjusted left padding */ - overflow-y: auto; - flex: 1; - `; + 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 (non-scrollable) --- + // --- 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; - `; - notification.appendChild(leftBar); // Add bar to main container + 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); - document.body.appendChild(notification); + // Add to DOM + container.appendChild(notification); // --- Keyframes and Timer Logic --- - const styleSheet = document.createElement("style"); - styleSheet.type = "text/css"; - styleSheet.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: translateX(20px); } to { opacity: 1; transform: translateX(0); } } - .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); } - `; + 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'); - document.head.appendChild(styleSheet); let dismissTimeout = null; - const startDismissTimer = () => { - dismissTimeout = window.setTimeout(() => { + 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(); + } } - }, duration); + }); + }; + closeBtn.onclick = closeNotification; + const startDismissTimer = () => { + dismissTimeout = window.setTimeout(closeNotification, duration); progressBar.style.animation = `lf-progress ${duration / 1000}s linear`; }; const pauseAndRewindTimer = () => { @@ -194,70 +178,43 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration } /** * Shows a success notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showSuccessNotification(message, duration = 3000) { showNotification(message, undefined, duration, "success"); } /** * Shows an error notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 5000) */ export function showErrorNotification(message, duration = 5000) { showNotification(message, undefined, duration, "error"); } /** * Shows an info notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showInfoNotification(message, duration = 3000) { showNotification(message, undefined, duration, "info"); } /** * Shows a warning notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showWarningNotification(message, duration = 3000) { showNotification(message, undefined, duration, "warning"); } /** * Shows an alert notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showAlertNotification(message, duration = 3000) { showNotification(message, undefined, duration, "alert"); } /** * Shows a sequence of all notification types for debugging purposes. - * @param message - An optional message to display in all notification types. */ export function showAllNotificationTypes(message) { const types = ["success", "error", "info", "warning", "alert"]; types.forEach((type, index) => { const notificationMessage = message || `This is a '${type}' notification.`; setTimeout(() => { - switch (type) { - case "success": - showSuccessNotification(notificationMessage); - break; - case "error": - showErrorNotification(notificationMessage); - break; - case "info": - showInfoNotification(notificationMessage); - break; - case "warning": - showWarningNotification(notificationMessage); - break; - case "alert": - showAlertNotification(notificationMessage); - break; - } + showNotification(notificationMessage, undefined, 3000, type); }, index * 400); // Stagger the notifications }); } diff --git a/src/CanvasState.ts b/src/CanvasState.ts index 82461d2..0aa89e0 100644 --- a/src/CanvasState.ts +++ b/src/CanvasState.ts @@ -1,6 +1,6 @@ import {getCanvasState, setCanvasState, saveImage, getImage} from "./db.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; -import {showAlertNotification} from "./utils/NotificationUtils.js"; +import {showAlertNotification, showAllNotificationTypes} from "./utils/NotificationUtils.js"; import {generateUUID, cloneLayers, getStateSignature, debounce, createCanvas} from "./utils/CommonUtils.js"; import {withErrorHandling} from "./ErrorHandler.js"; import type { Canvas } from './Canvas'; diff --git a/src/CanvasView.ts b/src/CanvasView.ts index f834fe2..3e07575 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -14,7 +14,7 @@ import {clearAllCanvasStates} from "./db.js"; import {ImageCache} from "./ImageCache.js"; import {generateUniqueFileName, createCanvas} from "./utils/CommonUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; -import {showErrorNotification, showSuccessNotification} from "./utils/NotificationUtils.js"; +import {showErrorNotification, showSuccessNotification, showInfoNotification, showWarningNotification} from "./utils/NotificationUtils.js"; import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js"; import { registerImageInClipspace, startSAMDetectorMonitoring, setupSAMDetectorHook } from "./SAMDetectorIntegration.js"; import type { ComfyNode, Layer, AddMode } from './types'; @@ -341,9 +341,13 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): const spinner = $el("div.matting-spinner") as HTMLDivElement; button.appendChild(spinner); button.classList.add('loading'); + + showInfoNotification("Starting background removal process...", 2000); try { - if (canvas.canvasSelection.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting."); + if (canvas.canvasSelection.selectedLayers.length !== 1) { + throw new Error("Please select exactly one image layer for matting."); + } const selectedLayer = canvas.canvasSelection.selectedLayers[0]; const selectedLayerIndex = canvas.layers.indexOf(selectedLayer); @@ -359,24 +363,28 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): if (!response.ok) { let errorMsg = `Server error: ${response.status} - ${response.statusText}`; if (result && result.error) { - errorMsg = `Error: ${result.error}\n\nDetails: ${result.details}`; + errorMsg = `Error: ${result.error}. Details: ${result.details || 'Check console'}`; } throw new Error(errorMsg); } + const mattedImage = new Image(); mattedImage.src = result.matted_image; await mattedImage.decode(); + const newLayer = {...selectedLayer, image: mattedImage, flipH: false, flipV: false} as Layer; delete (newLayer as any).imageId; + canvas.layers[selectedLayerIndex] = newLayer; canvas.canvasSelection.updateSelection([newLayer]); canvas.render(); canvas.saveState(); + showSuccessNotification("Background removed successfully!"); + } catch (error: any) { log.error("Matting error:", error); const errorMessage = error.message || "An unknown error occurred."; - const errorDetails = error.stack || (error.details ? JSON.stringify(error.details, null, 2) : "No details available."); - showErrorDialog(errorMessage, errorDetails); + showErrorNotification(`Matting Failed: ${errorMessage}`); } finally { button.classList.remove('loading'); if (button.contains(spinner)) { @@ -944,57 +952,6 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): }; } -function showErrorDialog(message: string, details: string) { - const dialog = $el("div.painter-dialog.error-dialog", { - style: { - position: 'fixed', - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)', - zIndex: '9999', - padding: '20px', - background: '#282828', - border: '1px solid #ff4444', - borderRadius: '8px', - minWidth: '400px', - maxWidth: '80vw', - } - }, [ - $el("h3", { textContent: "Matting Error", style: { color: "#ff4444", marginTop: "0" } }), - $el("p", { textContent: message, style: { color: "white" } }), - $el("pre.error-details", { - textContent: details, - style: { - background: "#1e1e1e", - border: "1px solid #444", - padding: "10px", - maxHeight: "300px", - overflowY: "auto", - whiteSpace: "pre-wrap", - wordBreak: "break-all", - color: "#ccc" - } - }), - $el("div.dialog-buttons", { style: { textAlign: "right", marginTop: "20px" } }, [ - $el("button", { - textContent: "Copy Details", - onclick: () => { - navigator.clipboard.writeText(details) - .then(() => showSuccessNotification("Error details copied to clipboard!")) - .catch(err => showErrorNotification("Failed to copy details: " + err)); - } - }), - $el("button", { - textContent: "Close", - style: { marginLeft: "10px" }, - onclick: () => document.body.removeChild(dialog) - }) - ]) - ]); - - document.body.appendChild(dialog); -} - const canvasNodeInstances = new Map(); app.registerExtension({ diff --git a/src/utils/NotificationUtils.ts b/src/utils/NotificationUtils.ts index 10ac062..a10fd87 100644 --- a/src/utils/NotificationUtils.ts +++ b/src/utils/NotificationUtils.ts @@ -23,44 +23,34 @@ export function showNotification( // Type-specific config const config = { - success: { - icon: "✔️", - title: "Success", - bg: "#1fd18b", - color: "#155c3b" - }, - error: { - icon: "❌", - title: "Error", - bg: "#ff6f6f", - color: "#7a2323" - }, - info: { - icon: "ℹ️", - title: "Info", - bg: "#4a6cd4", - color: "#fff" - }, - warning: { - icon: "⚠️", - title: "Warning", - bg: "#ffd43b", - color: "#7a5c00" - }, - alert: { - icon: "⚠️", - title: "Alert", - bg: "#fff7cc", - color: "#7a5c00" - } + 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]; - // --- Dark, modern notification style with sticky header --- + // --- 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 = ` - position: fixed; - top: 24px; - right: 24px; min-width: 380px; max-width: 440px; max-height: 80vh; @@ -68,46 +58,25 @@ export function showNotification( color: #fff; border-radius: 12px; box-shadow: 0 4px 32px rgba(0,0,0,0.25); - z-index: 10001; - font-size: 14px; display: flex; flex-direction: column; padding: 0; - margin-bottom: 18px; 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.2s; + 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; - `; + 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; - `; - + 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.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: ``, @@ -118,7 +87,6 @@ export function showNotification( 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; @@ -126,7 +94,6 @@ export function showNotification( 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'; @@ -135,13 +102,10 @@ export function showNotification( 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;`; - closeBtn.onclick = () => { if (notification.parentNode) notification.parentNode.removeChild(notification); }; - topRightContainer.appendChild(tag); topRightContainer.appendChild(closeBtn); @@ -151,59 +115,81 @@ export function showNotification( // --- Scrollable Body --- const body = document.createElement('div'); - body.style.cssText = ` - padding: 0px 20px 16px 20px; /* Adjusted left padding */ - overflow-y: auto; - flex: 1; - `; - + 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 (non-scrollable) --- - 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; - `; - notification.appendChild(leftBar); // Add bar to main container + // --- 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); - document.body.appendChild(notification); + + // Add to DOM + container.appendChild(notification); // --- Keyframes and Timer Logic --- - const styleSheet = document.createElement("style"); - styleSheet.type = "text/css"; - styleSheet.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: translateX(20px); } to { opacity: 1; transform: translateX(0); } } - .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); } - `; + const styleSheet = document.getElementById('lf-notification-styles') as HTMLStyleElement; + 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'); - document.head.appendChild(styleSheet); let dismissTimeout: number | null = null; - const startDismissTimer = () => { - dismissTimeout = window.setTimeout(() => { + 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(); + } } - }, duration); + }); + }; + + 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; @@ -211,17 +197,16 @@ export function showNotification( 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 - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showSuccessNotification(message: string, duration: number = 3000): void { showNotification(message, undefined, duration, "success"); @@ -229,8 +214,6 @@ export function showSuccessNotification(message: string, duration: number = 3000 /** * Shows an error notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 5000) */ export function showErrorNotification(message: string, duration: number = 5000): void { showNotification(message, undefined, duration, "error"); @@ -238,8 +221,6 @@ export function showErrorNotification(message: string, duration: number = 5000): /** * Shows an info notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showInfoNotification(message: string, duration: number = 3000): void { showNotification(message, undefined, duration, "info"); @@ -247,8 +228,6 @@ export function showInfoNotification(message: string, duration: number = 3000): /** * Shows a warning notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showWarningNotification(message: string, duration: number = 3000): void { showNotification(message, undefined, duration, "warning"); @@ -256,8 +235,6 @@ export function showWarningNotification(message: string, duration: number = 3000 /** * Shows an alert notification - * @param message - The message to show - * @param duration - Duration in milliseconds (default: 3000) */ export function showAlertNotification(message: string, duration: number = 3000): void { showNotification(message, undefined, duration, "alert"); @@ -265,31 +242,13 @@ export function showAlertNotification(message: string, duration: number = 3000): /** * Shows a sequence of all notification types for debugging purposes. - * @param message - An optional message to display in all notification types. */ export function showAllNotificationTypes(message?: string): void { const types: ("success" | "error" | "info" | "warning" | "alert")[] = ["success", "error", "info", "warning", "alert"]; - types.forEach((type, index) => { const notificationMessage = message || `This is a '${type}' notification.`; setTimeout(() => { - switch (type) { - case "success": - showSuccessNotification(notificationMessage); - break; - case "error": - showErrorNotification(notificationMessage); - break; - case "info": - showInfoNotification(notificationMessage); - break; - case "warning": - showWarningNotification(notificationMessage); - break; - case "alert": - showAlertNotification(notificationMessage); - break; - } + showNotification(notificationMessage, undefined, 3000, type); }, index * 400); // Stagger the notifications }); }