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
});
}