import { store } from "../core/store.js"; import { findActiveSidebars, findPropertiesPanel } from "../comfy/adapter.js"; export class SidebarOverlay { constructor(options = {}) { this.onDestroy = options.onDestroy || (() => {}); this.onUserClose = options.onUserClose || (() => {}); this.cleanupFns = []; this.observedElements = new Set(); // Elevate sidebars above overlay document.body.classList.add("comfy-sidebar-overlay-active"); // Root container: visual backdrop (covers 100% flush from sidebar to sidebar, zero gap) this.container = document.createElement("div"); this.container.className = `comfy-sidebar-comparison-overlay ${options.className || ""}`; Object.assign(this.container.style, { position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh", background: options.background || "color-mix(in srgb, var(--comfy-menu-bg, #121212) 95%, transparent)", backdropFilter: "blur(8px)", color: "var(--fg-color, #eee)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", zIndex: "1000", boxSizing: "border-box", overflow: "hidden", pointerEvents: "none", userSelect: "none" }); // Interactive backdrop: insets 6px from each sidebar so the border zones pass clicks to the resizers this.backdrop = document.createElement("div"); Object.assign(this.backdrop.style, { position: "absolute", top: "0", bottom: "0", left: "6px", right: "6px", pointerEvents: "auto", zIndex: "1" }); this.backdrop.addEventListener("click", (e) => { if (e.target === this.backdrop) { this.onUserClose(); this.destroy(); } }); this.container.appendChild(this.backdrop); // Close button this.closeBtn = document.createElement("span"); this.closeBtn.className = "pi pi-times"; this.closeBtn.title = "Close (Esc)"; Object.assign(this.closeBtn.style, { position: "absolute", top: "16px", right: "24px", zIndex: "30", cursor: "pointer", fontSize: "18px", color: "var(--desc-color, #aaa)", transition: "all 0.15s ease", background: "var(--comfy-input-bg, rgba(20, 20, 20, 0.6))", border: "1px solid var(--border-color, rgba(255, 255, 255, 0.1))", borderRadius: "50%", padding: "6px", pointerEvents: "auto" }); this.closeBtn.onmouseenter = () => { this.closeBtn.style.color = "var(--fg-color, #fff)"; this.closeBtn.style.borderColor = "var(--fg-color, #fff)"; }; this.closeBtn.onmouseleave = () => { this.closeBtn.style.color = "var(--desc-color, #aaa)"; this.closeBtn.style.borderColor = "var(--border-color, rgba(255, 255, 255, 0.1))"; }; this.closeBtn.onclick = () => { this.onUserClose(); this.destroy(); }; this.container.appendChild(this.closeBtn); // ResizeObserver to track live drag-resizing on all active sidebars if (window.ResizeObserver) { this.ro = new ResizeObserver(() => { this.updateOverlayBounds(); }); this.cleanupFns.push(() => this.ro.disconnect()); } // Sidebar boundary alignment this.updateOverlayBounds = this.updateOverlayBounds.bind(this); this.updateOverlayBounds(); window.addEventListener("resize", this.updateOverlayBounds); this.cleanupFns.push(() => window.removeEventListener("resize", this.updateOverlayBounds)); // Live observer for opening/closing panels or layout changes if (window.MutationObserver) { let updateScheduled = false; const mo = new MutationObserver((mutations) => { // Skip mutations caused purely by the sticky header's scroll animation const relevant = mutations.some((m) => { const el = m.target.nodeType === 1 ? m.target : m.target.parentElement; return !el?.closest?.(".comfy-sidebar-sticky-header-group"); }); if (!relevant) return; if (!updateScheduled) { updateScheduled = true; requestAnimationFrame(() => { this.updateOverlayBounds(); updateScheduled = false; }); } }); mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "class"] }); this.cleanupFns.push(() => mo.disconnect()); } // Global Key Handler (Esc to close) const onKeyDown = (e) => { if (e.key === "Escape") { e.preventDefault(); this.onUserClose(); this.destroy(); } if (options.onKeyDown) options.onKeyDown(e); }; document.addEventListener("keydown", onKeyDown); this.cleanupFns.push(() => document.removeEventListener("keydown", onKeyDown)); document.body.appendChild(this.container); } observeElement(el) { if (!el || !this.ro || this.observedElements.has(el)) return; this.observedElements.add(el); this.ro.observe(el); } updateOverlayBounds() { // A pending rAF/ResizeObserver callback can fire after destroy(); don't re-attach observers if (this.destroyed) return; let leftOffset = 0; let rightOffset = 0; let topOffset = 0; const winW = window.innerWidth; const midX = winW / 2; try { // Dynamically detect any top header/tab bar docked to the top of the screen const potentialTopBars = document.querySelectorAll("header, .comfyui-header, .comfyui-menu, .comfy-menu, [class*='tab-bar'], [class*='workflow-tab'], [class*='workspace-tab'], [class*='header']"); for (const bar of potentialTopBars) { // Ignore our own sliding gallery/queue header (it animates via translateY) if (bar.closest(".comfy-sidebar-sticky-header-group, .comfy-sidebar-header-root")) continue; // Must be an element sitting at the top of the viewport const r = bar.getBoundingClientRect(); if (r.top <= 2 && r.bottom > 20 && r.bottom < 120 && r.width > 200) { topOffset = Math.max(topOffset, Math.round(r.bottom)); } } const { leftPanels, rightPanels } = findActiveSidebars(); [...leftPanels, ...rightPanels].forEach(p => this.observeElement(p)); for (const el of leftPanels) { const rect = el.getBoundingClientRect(); if (rect.right > 0 && rect.right < midX) { leftOffset = Math.max(leftOffset, Math.round(rect.right)); } } for (const el of rightPanels) { const rect = el.getBoundingClientRect(); if (rect.left > midX && rect.left < winW) { rightOffset = Math.max(rightOffset, Math.round(winW - rect.left)); } } } catch (e) { console.error("Comfy Sidebar: Error finding active sidebars:", e); } // Safety clamp: offsets can never exceed half screen width leftOffset = Math.min(midX - 50, Math.max(0, leftOffset)); rightOffset = Math.min(midX - 50, Math.max(0, rightOffset)); topOffset = Math.max(0, topOffset); this.container.style.left = `${leftOffset}px`; this.container.style.right = `${rightOffset}px`; this.container.style.width = `calc(100vw - ${leftOffset + rightOffset}px)`; this.container.style.top = `${topOffset}px`; this.container.style.height = `calc(100vh - ${topOffset}px)`; // Crisp 1px visible separator lines along active sidebar borders this.container.style.borderLeft = leftOffset > 0 ? "1px solid var(--border-color, rgba(255, 255, 255, 0.22))" : "none"; this.container.style.borderRight = rightOffset > 0 ? "1px solid var(--border-color, rgba(255, 255, 255, 0.22))" : "none"; } addCleanup(fn) { this.cleanupFns.push(fn); } destroy() { if (this.destroyed) return; this.destroyed = true; document.body.classList.remove("comfy-sidebar-overlay-active"); for (const fn of this.cleanupFns.splice(0)) { try { fn(); } catch (e) {} } this.container.remove(); this.onDestroy(); } }