/** * Centralized Window System for Addons * Provides a unified API for creating and managing draggable, resizable windows */ let nextZIndex = 1000; let windowCount = 0; const activeWindows = new Map(); class AddonWindow { constructor (options = {}) { this.id = options.id || `addon-window-${++windowCount}`; this.title = options.title || 'Addon Window'; this.width = options.width || 400; this.height = options.height || 300; this.minWidth = options.minWidth || 200; this.minHeight = options.minHeight || 150; this.maxWidth = options.maxWidth || null; this.maxHeight = options.maxHeight || null; this.x = options.x || (Math.random() * 100) + 50; this.y = options.y || (Math.random() * 100) + 50; this.resizable = options.resizable !== false; this.modal = options.modal || false; this.closable = options.closable !== false; this.minimizable = options.minimizable !== false; this.maximizable = options.maximizable !== false; this.className = options.className || ''; this.isVisible = false; this.isMinimized = false; this.isMaximized = false; this.zIndex = ++nextZIndex; this.onClose = options.onClose || (() => {}); this.onMinimize = options.onMinimize || (() => {}); this.onMaximize = options.onMaximize || (() => {}); this.onRestore = options.onRestore || (() => {}); this.onResize = options.onResize || (() => {}); this.onMove = options.onMove || (() => {}); this.element = null; this.headerElement = null; this.contentElement = null; this.isDragging = false; this.isResizing = false; this.dragOffset = {x: 0, y: 0}; this.savedState = null; // For maximize/restore this.createWindow(); activeWindows.set(this.id, this); } createWindow () { // Create main window element this.element = document.createElement('div'); this.element.className = `addon-window ${this.className}`; this.element.style.cssText = ` position: fixed; left: ${this.x}px; top: ${this.y}px; width: ${this.width}px; height: ${this.height}px; z-index: ${this.zIndex}; background: linear-gradient(135deg, var(--ui-modal-background, #ffffff) 0%, var(--ui-primary, #f8f9fa) 100%); border: 1px solid var(--ui-black-transparent, rgba(0, 0, 0, 0.08)); border-radius: 12px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 15px 12px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.2) inset; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; display: none; flex-direction: column; overflow: hidden; backdrop-filter: blur(20px); transition: none !important; `; this.element.addEventListener('mousedown', () => this.bringToFront()); // Add focus enhancement when window becomes active this.element.addEventListener('mouseenter', () => { if (this.isVisible) { this.element.style.boxShadow = ` 0 25px 50px rgba(0, 0, 0, 0.15), 0 20px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(255, 255, 255, 0.3) inset `; } }); this.element.addEventListener('mouseleave', () => { if (this.isVisible && !this.isDragging && !this.isResizing) { this.element.style.boxShadow = ` 0 20px 40px rgba(0, 0, 0, 0.1), 0 15px 12px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.2) inset `; } }); // Create header this.headerElement = document.createElement('div'); this.headerElement.className = 'addon-window-header'; this.headerElement.style.cssText = ` background: linear-gradient(135deg, var(--ui-secondary, #f8f9fa) 0%, var(--ui-primary, #ffffff) 100%); border-bottom: 1px solid var(--ui-black-transparent, rgba(0, 0, 0, 0.08)); padding: 8px 16px; cursor: move; user-select: none; display: flex; align-items: center; justify-content: space-between; min-height: 44px; box-sizing: border-box; backdrop-filter: blur(10px); position: relative; overflow: hidden; `; // Add subtle header gradient overlay const headerOverlay = document.createElement('div'); headerOverlay.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%); pointer-events: none; `; this.headerElement.appendChild(headerOverlay); // Title const titleElement = document.createElement('div'); titleElement.className = 'addon-window-title'; titleElement.textContent = this.title; titleElement.style.cssText = ` font-weight: 600; font-size: 14px; color: var(--text-primary, #2d3748); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8); z-index: 1; `; // Controls const controlsElement = document.createElement('div'); controlsElement.className = 'addon-window-controls'; controlsElement.style.cssText = ` display: flex; gap: 6px; align-items: center; z-index: 1; overflow: hidden; `; // Control buttons if (this.maximizable) { const maximizeBtn = this.createControlButton('maximize', 'Maximize', () => this.toggleMaximize()); this.maximizeBtn = maximizeBtn; // Store reference to update icon when maximized controlsElement.appendChild(maximizeBtn); } if (this.closable) { const closeBtn = this.createControlButton('close', 'Minimize', () => this.minimize()); controlsElement.appendChild(closeBtn); } this.headerElement.appendChild(titleElement); this.headerElement.appendChild(controlsElement); // Create content area this.contentElement = document.createElement('div'); this.contentElement.className = 'addon-window-content'; this.contentElement.style.cssText = ` flex: 1; overflow: auto; padding: 0; box-sizing: border-box; background: linear-gradient(135deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%); border-radius: 0 0 12px 12px; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; min-height: 0; max-height: 100%; display: flex; flex-direction: column; scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.2) transparent; `; // Add custom scrollbar styling this.addScrollbarStyling(this.contentElement); this.element.appendChild(this.headerElement); this.element.appendChild(this.contentElement); // Add resize handles if resizable if (this.resizable) { this.addResizeHandles(); } // Add drag functionality this.addDragFunctionality(); // Add to DOM document.body.appendChild(this.element); } createControlButton (type, title, onClick) { const button = document.createElement('button'); button.title = title; button.className = `addon-window-btn addon-window-btn-${type}`; // Create SVG icon based on button type let svgIcon = ''; switch (type) { case 'maximize': svgIcon = ` `; break; case 'restore': svgIcon = ` `; break; case 'close': svgIcon = ` `; break; } button.innerHTML = svgIcon; // Modern button styling button.style.cssText = ` background: transparent; border: none; cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 6px; color: var(--text-primary, #666); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; font-size: 0; margin: 0; padding: 0; `; // Add shimmer effect const shimmer = document.createElement('div'); shimmer.style.cssText = ` position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%); transition: left 0.5s ease; pointer-events: none; `; button.appendChild(shimmer); // Hover effects button.addEventListener('mouseenter', () => { if (type === 'close') { button.style.background = 'linear-gradient(135deg, #ffbd2e 0%, #ffa500 100%)'; button.style.color = 'white'; button.style.transform = 'scale(1.05)'; button.style.boxShadow = '0 4px 12px rgba(255, 189, 46, 0.4)'; } else { button.style.background = 'linear-gradient(135deg, #28ca42 0%, #20a935 100%)'; button.style.color = 'white'; button.style.transform = 'scale(1.05)'; button.style.boxShadow = '0 4px 12px rgba(40, 202, 66, 0.4)'; } shimmer.style.left = '100%'; }); button.addEventListener('mouseleave', () => { button.style.background = 'transparent'; button.style.color = 'var(--text-primary, #666)'; button.style.transform = 'scale(1)'; button.style.boxShadow = 'none'; shimmer.style.left = '-100%'; }); button.addEventListener('mousedown', e => { e.stopPropagation(); button.style.transform = 'scale(0.95)'; }); button.addEventListener('mouseup', () => { button.style.transform = 'scale(1.05)'; }); button.addEventListener('click', e => { e.stopPropagation(); onClick(); }); // Focus handling for accessibility button.addEventListener('focus', () => { button.style.outline = '2px solid var(--looks-secondary, #4C97FF)'; button.style.outlineOffset = '2px'; }); button.addEventListener('blur', () => { button.style.outline = 'none'; }); return button; } updateMaximizeButton () { if (this.maximizeBtn) { const svgIcon = this.isMaximized ? ` ` : ` `; this.maximizeBtn.innerHTML = svgIcon; this.maximizeBtn.title = this.isMaximized ? 'Restore' : 'Maximize'; } } addDragFunctionality () { this.headerElement.addEventListener('mousedown', e => { if (e.target.tagName === 'BUTTON') return; this.isDragging = true; this.bringToFront(); // Get the current position of the window const currentX = parseInt(this.element.style.left, 10) || this.x; const currentY = parseInt(this.element.style.top, 10) || this.y; // Calculate offset relative to current window position this.dragOffset = { x: e.clientX - currentX, y: e.clientY - currentY }; document.addEventListener('mousemove', this.handleDrag); document.addEventListener('mouseup', this.handleDragEnd); e.preventDefault(); }); } handleDrag = e => { if (!this.isDragging) return; const newX = e.clientX - this.dragOffset.x; const newY = e.clientY - this.dragOffset.y; // Allow window to move mostly off-screen but keep 50px visible // Don't allow the top of the window to go above the top of the page const minVisiblePixels = 50; const minX = -(this.width - minVisiblePixels); const maxX = window.innerWidth - minVisiblePixels; const minY = 0; // Don't allow window top to go above page top const maxY = window.innerHeight - minVisiblePixels; this.x = Math.max(minX, Math.min(newX, maxX)); this.y = Math.max(minY, Math.min(newY, maxY)); this.element.style.left = `${this.x}px`; this.element.style.top = `${this.y}px`; this.onMove(this.x, this.y); }; handleDragEnd = () => { this.isDragging = false; document.removeEventListener('mousemove', this.handleDrag); document.removeEventListener('mouseup', this.handleDragEnd); }; addResizeHandles () { const handles = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']; handles.forEach(direction => { const handle = document.createElement('div'); handle.className = `resize-handle resize-${direction}`; const styles = { position: 'absolute', backgroundColor: 'transparent', zIndex: '10' }; // Set position and cursor for each handle switch (direction) { case 'n': Object.assign(styles, { top: '0', left: '8px', right: '8px', height: '4px', cursor: 'n-resize' }); break; case 'ne': Object.assign(styles, { top: '0', right: '0', width: '8px', height: '8px', cursor: 'ne-resize' }); break; case 'e': Object.assign(styles, { right: '0', top: '8px', bottom: '8px', width: '4px', cursor: 'e-resize' }); break; case 'se': Object.assign(styles, { bottom: '0', right: '0', width: '8px', height: '8px', cursor: 'se-resize' }); break; case 's': Object.assign(styles, { bottom: '0', left: '8px', right: '8px', height: '4px', cursor: 's-resize' }); break; case 'sw': Object.assign(styles, { bottom: '0', left: '0', width: '8px', height: '8px', cursor: 'sw-resize' }); break; case 'w': Object.assign(styles, { left: '0', top: '8px', bottom: '8px', width: '4px', cursor: 'w-resize' }); break; case 'nw': Object.assign(styles, { top: '0', left: '0', width: '8px', height: '8px', cursor: 'nw-resize' }); break; } Object.assign(handle.style, styles); handle.addEventListener('mousedown', e => { e.stopPropagation(); this.startResize(e, direction); }); this.element.appendChild(handle); }); } startResize (e, direction) { this.isResizing = true; this.resizeDirection = direction; this.bringToFront(); const rect = this.element.getBoundingClientRect(); this.resizeStart = { x: e.clientX, y: e.clientY, width: rect.width, height: rect.height, left: rect.left, top: rect.top }; document.addEventListener('mousemove', this.handleResize); document.addEventListener('mouseup', this.handleResizeEnd); e.preventDefault(); } handleResize = e => { if (!this.isResizing) return; const deltaX = e.clientX - this.resizeStart.x; const deltaY = e.clientY - this.resizeStart.y; const direction = this.resizeDirection; let newWidth = this.resizeStart.width; let newHeight = this.resizeStart.height; let newX = this.resizeStart.left; let newY = this.resizeStart.top; // Calculate new dimensions based on resize direction if (direction.includes('e')) newWidth += deltaX; if (direction.includes('w')) { newWidth -= deltaX; newX = this.resizeStart.left + deltaX; } if (direction.includes('s')) newHeight += deltaY; if (direction.includes('n')) { newHeight -= deltaY; newY = this.resizeStart.top + deltaY; } // Apply constraints const originalNewWidth = newWidth; const originalNewHeight = newHeight; newWidth = Math.max(this.minWidth, newWidth); newHeight = Math.max(this.minHeight, newHeight); if (this.maxWidth) newWidth = Math.min(this.maxWidth, newWidth); if (this.maxHeight) newHeight = Math.min(this.maxHeight, newHeight); // Adjust position if size was constrained and we're resizing from west or north if (direction.includes('w') && newWidth !== originalNewWidth) { newX = this.resizeStart.left + (this.resizeStart.width - newWidth); } if (direction.includes('n') && newHeight !== originalNewHeight) { newY = this.resizeStart.top + (this.resizeStart.height - newHeight); } // Update dimensions this.width = newWidth; this.height = newHeight; this.x = newX; this.y = newY; this.element.style.width = `${newWidth}px`; this.element.style.height = `${newHeight}px`; this.element.style.left = `${newX}px`; this.element.style.top = `${newY}px`; this.onResize(newWidth, newHeight); }; handleResizeEnd = () => { this.isResizing = false; document.removeEventListener('mousemove', this.handleResize); document.removeEventListener('mouseup', this.handleResizeEnd); }; addScrollbarStyling () { // Create a style element for custom scrollbars const style = document.createElement('style'); style.textContent = ` .addon-window-content { scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.2) transparent; } .addon-window-content::-webkit-scrollbar { width: 12px; height: 12px; } .addon-window-content::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.03); border-radius: 6px; margin: 2px; } .addon-window-content::-webkit-scrollbar-thumb { background: linear-gradient(135deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.15) 100%); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; transition: all 0.3s ease; min-height: 20px; } .addon-window-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0.25) 100%); background-clip: content-box; } .addon-window-content::-webkit-scrollbar-thumb:active { background: linear-gradient(135deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.35) 100%); background-clip: content-box; } .addon-window-content::-webkit-scrollbar-corner { background: transparent; } `; document.head.appendChild(style); this.scrollbarStyle = style; // Store reference for cleanup } bringToFront () { this.zIndex = ++nextZIndex; this.element.style.zIndex = this.zIndex; } show () { this.isVisible = true; this.element.style.display = 'flex'; this.bringToFront(); return this; } hide () { this.isVisible = false; this.element.style.display = 'none'; return this; } close () { this.hide(); this.onClose(); activeWindows.delete(this.id); if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } } minimize () { this.hide(); this.isMinimized = true; this.onMinimize(); this.updateMaximizeButton(); return this; } restore () { if (this.isMaximized) { this.isMaximized = false; if (this.savedState) { this.x = this.savedState.x; this.y = this.savedState.y; this.width = this.savedState.width; this.height = this.savedState.height; this.element.style.left = `${this.x}px`; this.element.style.top = `${this.y}px`; this.element.style.width = `${this.width}px`; this.element.style.height = `${this.height}px`; } this.updateMaximizeButton(); } if (this.isMinimized) { this.isMinimized = false; this.show(); } this.onRestore(); return this; } maximize () { if (this.isMaximized) return this; // Save current state this.savedState = { x: this.x, y: this.y, width: this.width, height: this.height }; this.isMaximized = true; this.x = 0; this.y = 0; this.width = window.innerWidth; this.height = window.innerHeight; this.element.style.left = '0px'; this.element.style.top = '0px'; this.element.style.width = '100vw'; this.element.style.height = '100vh'; this.updateMaximizeButton(); this.onMaximize(); return this; } toggleMaximize () { if (this.isMaximized) { this.restore(); } else { this.maximize(); } return this; } setContent (content) { this.contentElement.innerHTML = ''; if (typeof content === 'string') { this.contentElement.innerHTML = content; } else if (content instanceof HTMLElement) { this.contentElement.appendChild(content); } return this; } setTitle (newTitle) { this.title = newTitle; const titleElement = this.headerElement.querySelector('.addon-window-title'); if (titleElement) { titleElement.textContent = newTitle; } return this; } getContentElement () { return this.contentElement; } center () { this.x = (window.innerWidth - this.width) / 2; this.y = (window.innerHeight - this.height) / 2; this.element.style.left = `${this.x}px`; this.element.style.top = `${this.y}px`; return this; } // Compatibility methods for external code focus () { this.bringToFront(); return this; } isClosed () { return !this.isVisible; } } // Window Manager API const WindowManager = { createWindow (options) { return new AddonWindow(options); }, getWindow (id) { return activeWindows.get(id); }, getAllWindows () { return Array.from(activeWindows.values()); }, closeWindow (id) { const window = activeWindows.get(id); if (window) { window.close(); } }, closeAllWindows () { for (const window of activeWindows.values()) { window.close(); } }, bringToFront (id) { const window = activeWindows.get(id); if (window) { window.bringToFront(); } } }; export default WindowManager;