mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Enhance notification system and auto-correct node_id
Adds a modern, type-based notification UI with support for success, error, info, warning, and alert styles, including a new showAlertNotification function. CanvasState now auto-corrects the node_id widget before saving state and notifies the user if a correction occurs. CanvasView centering logic now uses the actual canvas container for more accurate viewport adjustments.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getCanvasState, setCanvasState, saveImage, getImage } from "./db.js";
|
||||
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||
import { showAlertNotification } from "./utils/NotificationUtils.js";
|
||||
import { generateUUID, cloneLayers, getStateSignature, debounce, createCanvas } from "./utils/CommonUtils.js";
|
||||
const log = createModuleLogger('CanvasState');
|
||||
export class CanvasState {
|
||||
@@ -237,6 +238,20 @@ export class CanvasState {
|
||||
log.error("Node ID is not available for saving state to DB.");
|
||||
return;
|
||||
}
|
||||
// Auto-correct node_id widget if needed before saving state
|
||||
if (this.canvas.node && this.canvas.node.widgets) {
|
||||
const nodeIdWidget = this.canvas.node.widgets.find((w) => w.name === "node_id");
|
||||
if (nodeIdWidget) {
|
||||
const correctId = String(this.canvas.node.id);
|
||||
if (nodeIdWidget.value !== correctId) {
|
||||
const prevValue = nodeIdWidget.value;
|
||||
nodeIdWidget.value = correctId;
|
||||
log.warn(`[CanvasState] node_id widget value (${prevValue}) did not match node.id (${correctId}) - auto-corrected (saveStateToDB).`);
|
||||
showAlertNotification(`The value of node_id (${prevValue}) did not match the node number (${correctId}) and was automatically corrected.
|
||||
If you see dark images or masks in the output, make sure node_id is set to ${correctId}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Preparing state to be sent to worker...");
|
||||
const layers = await this._prepareLayers();
|
||||
const state = {
|
||||
|
||||
@@ -774,6 +774,11 @@ async function createCanvasWidget(node, widget, app) {
|
||||
// Remove ESC key listener when editor closes
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
setTimeout(() => {
|
||||
// Use the actual canvas container for centering calculation
|
||||
const currentCanvasContainer = originalParent.querySelector('.painterCanvasContainer.painter-container');
|
||||
const fullscreenCanvasContainer = backdrop.querySelector('.painterCanvasContainer.painter-container');
|
||||
const currentRect = currentCanvasContainer.getBoundingClientRect();
|
||||
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
|
||||
adjustViewportForCentering(currentRect, fullscreenRect, -1);
|
||||
canvas.render();
|
||||
if (node.onResize) {
|
||||
@@ -811,8 +816,11 @@ async function createCanvasWidget(node, widget, app) {
|
||||
// Add ESC key listener when editor opens
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
setTimeout(() => {
|
||||
const originalRect = originalParent.getBoundingClientRect();
|
||||
const fullscreenRect = modalContent.getBoundingClientRect();
|
||||
// Use the actual canvas container for centering calculation
|
||||
const originalCanvasContainer = originalParent.querySelector('.painterCanvasContainer.painter-container');
|
||||
const fullscreenCanvasContainer = modalContent.querySelector('.painterCanvasContainer.painter-container');
|
||||
const originalRect = originalCanvasContainer.getBoundingClientRect();
|
||||
const fullscreenRect = fullscreenCanvasContainer.getBoundingClientRect();
|
||||
adjustViewportForCentering(originalRect, fullscreenRect, 1);
|
||||
canvas.render();
|
||||
if (node.onResize) {
|
||||
|
||||
@@ -9,28 +9,188 @@ const log = createModuleLogger('NotificationUtils');
|
||||
* @param backgroundColor - Background color (default: #4a6cd4)
|
||||
* @param duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
export function showNotification(message, backgroundColor = "#4a6cd4", duration = 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",
|
||||
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"
|
||||
}
|
||||
}[type];
|
||||
// --- Dark, modern notification style with sticky header ---
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${backgroundColor};
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
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);
|
||||
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;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
// --- 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: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-succ"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 4 L44 14 L44 34 L24 44 L4 34 L4 14 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/><g filter="url(#f-succ)"><path d="M16 24 L22 30 L34 18" stroke="#fff" stroke-width="3" fill="none"/></g></svg>`,
|
||||
error: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-err"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M14 14 L34 34 M34 14 L14 34" fill="none" stroke="#fff" stroke-width="3"/><g filter="url(#f-err)"><path d="M24,4 L42,12 L42,36 L24,44 L6,36 L6,12 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`,
|
||||
info: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-info"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 14 L24 16 M24 22 L24 34" stroke="#fff" stroke-width="3" fill="none"/><g filter="url(#f-info)"><path d="M12,4 L36,4 L44,12 L44,36 L36,44 L12,44 L4,36 L4,12 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`,
|
||||
warning: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-warn"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 14 L24 28 M24 34 L24 36" stroke="#fff" stroke-width="3" fill="none"/><g filter="url(#f-warn)"><path d="M24,4 L46,24 L24,44 L2,24 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`,
|
||||
alert: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-alert"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 14 L24 28 M24 34 L24 36" stroke="#fff" stroke-width="3" fill="none"/><g filter="url(#f-alert)"><path d="M24,4 L46,24 L24,44 L2,24 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`
|
||||
}[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;`;
|
||||
closeBtn.onclick = () => { if (notification.parentNode)
|
||||
notification.parentNode.removeChild(notification); };
|
||||
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; /* Adjusted left padding */
|
||||
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
|
||||
notification.appendChild(header);
|
||||
notification.appendChild(body);
|
||||
notification.appendChild(progressBar);
|
||||
document.body.appendChild(notification);
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, duration);
|
||||
log.debug(`Notification shown: ${message}`);
|
||||
// --- 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); }
|
||||
`;
|
||||
body.classList.add('notification-scrollbar');
|
||||
document.head.appendChild(styleSheet);
|
||||
let dismissTimeout = null;
|
||||
const startDismissTimer = () => {
|
||||
dismissTimeout = window.setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 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
|
||||
@@ -38,7 +198,7 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration
|
||||
* @param duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
export function showSuccessNotification(message, duration = 3000) {
|
||||
showNotification(message, "#4a7c59", duration);
|
||||
showNotification(message, undefined, duration, "success");
|
||||
}
|
||||
/**
|
||||
* Shows an error notification
|
||||
@@ -46,7 +206,7 @@ export function showSuccessNotification(message, duration = 3000) {
|
||||
* @param duration - Duration in milliseconds (default: 5000)
|
||||
*/
|
||||
export function showErrorNotification(message, duration = 5000) {
|
||||
showNotification(message, "#c54747", duration);
|
||||
showNotification(message, undefined, duration, "error");
|
||||
}
|
||||
/**
|
||||
* Shows an info notification
|
||||
@@ -54,5 +214,50 @@ export function showErrorNotification(message, duration = 5000) {
|
||||
* @param duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
export function showInfoNotification(message, duration = 3000) {
|
||||
showNotification(message, "#4a6cd4", duration);
|
||||
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;
|
||||
}
|
||||
}, index * 400); // Stagger the notifications
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {getCanvasState, setCanvasState, saveImage, getImage} from "./db.js";
|
||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||
import {showAlertNotification} from "./utils/NotificationUtils.js";
|
||||
import {generateUUID, cloneLayers, getStateSignature, debounce, createCanvas} from "./utils/CommonUtils.js";
|
||||
import {withErrorHandling} from "./ErrorHandler.js";
|
||||
import type { Canvas } from './Canvas';
|
||||
@@ -272,6 +273,23 @@ export class CanvasState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-correct node_id widget if needed before saving state
|
||||
if (this.canvas.node && this.canvas.node.widgets) {
|
||||
const nodeIdWidget = this.canvas.node.widgets.find((w: any) => w.name === "node_id");
|
||||
if (nodeIdWidget) {
|
||||
const correctId = String(this.canvas.node.id);
|
||||
if (nodeIdWidget.value !== correctId) {
|
||||
const prevValue = nodeIdWidget.value;
|
||||
nodeIdWidget.value = correctId;
|
||||
log.warn(`[CanvasState] node_id widget value (${prevValue}) did not match node.id (${correctId}) - auto-corrected (saveStateToDB).`);
|
||||
showAlertNotification(
|
||||
`The value of node_id (${prevValue}) did not match the node number (${correctId}) and was automatically corrected.
|
||||
If you see dark images or masks in the output, make sure node_id is set to ${correctId}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Preparing state to be sent to worker...");
|
||||
const layers = await this._prepareLayers();
|
||||
const state = {
|
||||
|
||||
@@ -12,30 +12,210 @@ const log = createModuleLogger('NotificationUtils');
|
||||
* @param backgroundColor - Background color (default: #4a6cd4)
|
||||
* @param duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
export function showNotification(message: string, backgroundColor: string = "#4a6cd4", duration: number = 3000): void {
|
||||
export function showNotification(
|
||||
message: string,
|
||||
backgroundColor: string = "#4a6cd4",
|
||||
duration: number = 3000,
|
||||
type: "success" | "error" | "info" | "warning" | "alert" = "info"
|
||||
): void {
|
||||
// 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",
|
||||
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"
|
||||
}
|
||||
}[type];
|
||||
|
||||
// --- Dark, modern notification style with sticky header ---
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${backgroundColor};
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
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);
|
||||
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;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
|
||||
// --- 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: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-succ"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 4 L44 14 L44 34 L24 44 L4 34 L4 14 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/><g filter="url(#f-succ)"><path d="M16 24 L22 30 L34 18" stroke="#fff" stroke-width="3" fill="none"/></g></svg>`,
|
||||
error: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-err"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M14 14 L34 34 M34 14 L14 34" fill="none" stroke="#fff" stroke-width="3"/><g filter="url(#f-err)"><path d="M24,4 L42,12 L42,36 L24,44 L6,36 L6,12 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`,
|
||||
info: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-info"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 14 L24 16 M24 22 L24 34" stroke="#fff" stroke-width="3" fill="none"/><g filter="url(#f-info)"><path d="M12,4 L36,4 L44,12 L44,36 L36,44 L12,44 L4,36 L4,12 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`,
|
||||
warning: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-warn"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 14 L24 28 M24 34 L24 36" stroke="#fff" stroke-width="3" fill="none"/><g filter="url(#f-warn)"><path d="M24,4 L46,24 L24,44 L2,24 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`,
|
||||
alert: `<svg width="48" height="48" viewBox="0 0 48 48"><defs><filter id="f-alert"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="${config.bg}"/></filter></defs><path d="M24 14 L24 28 M24 34 L24 36" stroke="#fff" stroke-width="3" fill="none"/><g filter="url(#f-alert)"><path d="M24,4 L46,24 L24,44 L2,24 Z" fill="rgba(255,255,255,0.08)" stroke="${config.bg}" stroke-width="2"/></g></svg>`
|
||||
}[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: string): string => {
|
||||
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;`;
|
||||
closeBtn.onclick = () => { if (notification.parentNode) notification.parentNode.removeChild(notification); };
|
||||
|
||||
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; /* Adjusted left padding */
|
||||
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
|
||||
notification.appendChild(header);
|
||||
notification.appendChild(body);
|
||||
notification.appendChild(progressBar);
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, duration);
|
||||
|
||||
log.debug(`Notification shown: ${message}`);
|
||||
|
||||
// --- 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); }
|
||||
`;
|
||||
body.classList.add('notification-scrollbar');
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
let dismissTimeout: number | null = null;
|
||||
const startDismissTimer = () => {
|
||||
dismissTimeout = window.setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +224,7 @@ export function showNotification(message: string, backgroundColor: string = "#4a
|
||||
* @param duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
export function showSuccessNotification(message: string, duration: number = 3000): void {
|
||||
showNotification(message, "#4a7c59", duration);
|
||||
showNotification(message, undefined, duration, "success");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +233,7 @@ export function showSuccessNotification(message: string, duration: number = 3000
|
||||
* @param duration - Duration in milliseconds (default: 5000)
|
||||
*/
|
||||
export function showErrorNotification(message: string, duration: number = 5000): void {
|
||||
showNotification(message, "#c54747", duration);
|
||||
showNotification(message, undefined, duration, "error");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,5 +242,54 @@ export function showErrorNotification(message: string, duration: number = 5000):
|
||||
* @param duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
export function showInfoNotification(message: string, duration: number = 3000): void {
|
||||
showNotification(message, "#4a6cd4", duration);
|
||||
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: string, duration: number = 3000): void {
|
||||
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: string, duration: number = 3000): void {
|
||||
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?: 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;
|
||||
}
|
||||
}, index * 400); // Stagger the notifications
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user