From 8bdfbedf4690308857d2701ec5996e50c6b0de2e Mon Sep 17 00:00:00 2001 From: AEmotionStudio <163354043+AEmotionStudio@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:02:06 -0800 Subject: [PATCH] feat: add new themes and enhance popout viewer interaction - Added 'Obsidian', 'Obsidian Dark', and 'Milk White' themes to Info Panel and PopOut viewer. - Improved theme detection in 'StateManager' to support new themes. - Implemented button widget support in PopOut viewer with 'button-click' event handling. - Enhanced widget options handling in 'PopOutManager' for combo widgets. - Updated dropdown accessibility tests. --- css/info-panel.css | 105 ++++++++- src/info-panel/StateManager.ts | 16 +- src/magnify-glass/PopOutManager.ts | 62 +++++- tests/unit/UIManager_Dropdown.test.ts | 46 ++-- web/info-panel.css | 105 ++++++++- web/info-panel/StateManager.js | 16 +- web/info-panel/StateManager.js.map | 2 +- web/magnify-glass/PopOutManager.js | 50 ++++- web/magnify-glass/PopOutManager.js.map | 2 +- web/popout-viewer.html | 282 +++++++++++++++++++++---- 10 files changed, 597 insertions(+), 89 deletions(-) diff --git a/css/info-panel.css b/css/info-panel.css index 9c78a95..12ab388 100644 --- a/css/info-panel.css +++ b/css/info-panel.css @@ -59,7 +59,7 @@ } .magnify-info-panel.theme-dark { - background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + background: linear-gradient(135deg, rgb(0, 0, 0), rgb(10, 10, 15)); color: var(--info-panel-text-color, #e0e0e0); border: 1px solid rgba(100, 100, 120, 0.4); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); @@ -67,7 +67,7 @@ } .magnify-info-panel.theme-light { - background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + background: linear-gradient(135deg, rgb(250, 250, 255), rgb(240, 240, 250)); color: var(--info-panel-text-color, #2a2a2a); border: 1px solid rgba(200, 200, 220, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); @@ -76,7 +76,7 @@ /* Solarized Theme - warm, muted tones */ .magnify-info-panel.theme-solarized { - background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96)); + background: linear-gradient(135deg, rgb(0, 43, 54), rgb(7, 54, 66)); color: var(--info-panel-text-color, #839496); border: 1px solid rgba(88, 110, 117, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); @@ -86,7 +86,7 @@ /* Arc Theme - modern blue-gray */ .magnify-info-panel.theme-arc { - background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96)); + background: linear-gradient(135deg, rgb(56, 60, 74), rgb(64, 69, 82)); color: var(--info-panel-text-color, #d3dae3); border: 1px solid rgba(90, 100, 120, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); @@ -96,7 +96,7 @@ /* Nord Theme - cool arctic colors */ .magnify-info-panel.theme-nord { - background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96)); + background: linear-gradient(135deg, rgb(46, 52, 64), rgb(59, 66, 82)); color: var(--info-panel-text-color, #d8dee9); border: 1px solid rgba(76, 86, 106, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); @@ -106,7 +106,7 @@ /* GitHub Theme - clean, professional */ .magnify-info-panel.theme-github { - background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96)); + background: linear-gradient(135deg, rgb(255, 255, 255), rgb(246, 248, 250)); color: var(--info-panel-text-color, #24292e); border: 1px solid rgba(225, 228, 232, 0.8); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); @@ -114,6 +114,37 @@ --info-panel-accent-color: #0366d6; } +/* Obsidian Theme - deep purple/charcoal */ +.magnify-info-panel.theme-obsidian { + background: linear-gradient(135deg, rgb(30, 30, 40), rgb(40, 40, 55)); + color: var(--info-panel-text-color, #d4d4d8); + border: 1px solid rgba(80, 80, 100, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + --info-panel-accent-color: #8b5cf6; +} + +/* Obsidian Dark Theme - even darker obsidian */ +.magnify-info-panel.theme-obsidian-dark { + background: linear-gradient(135deg, rgb(15, 15, 20), rgb(25, 25, 35)); + color: var(--info-panel-text-color, #c4c4cc); + border: 1px solid rgba(60, 60, 80, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 16px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(12px); + --info-panel-accent-color: #a78bfa; +} + +/* Milk White Theme - soft cream/white */ +.magnify-info-panel.theme-milk-white { + background: linear-gradient(135deg, rgb(255, 253, 250), rgb(250, 248, 245)); + color: var(--info-panel-text-color, #3d3d3d); + border: 1px solid rgba(220, 215, 210, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 16px rgba(0, 0, 0, 0.04); + backdrop-filter: blur(12px); + --info-panel-accent-color: #7c3aed; +} + + .magnify-info-panel.visible { transform: translateY(0) !important; } @@ -1265,6 +1296,68 @@ color: var(--info-panel-accent-color, #0366d6); } +/* Milk White Theme - light editor adjustments */ +.magnify-info-panel.theme-milk-white .widget-editor-input, +.magnify-info-panel.theme-milk-white .widget-editor-textarea { + background: rgba(255, 255, 255, 0.8); + border-color: rgba(180, 175, 170, 0.4); + color: #3d3d3d; +} + +.magnify-info-panel.theme-milk-white .widget-editor-input:focus, +.magnify-info-panel.theme-milk-white .widget-editor-textarea:focus { + border-color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .widget-editor-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .widget-editor-select:hover { + background-color: rgba(124, 58, 237, 0.08); +} + +.magnify-info-panel.theme-milk-white .widget-editor-select option { + background: #fffdfb; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .widget-editor-stepper { + border-color: rgba(0, 0, 0, 0.12); + background: rgba(0, 0, 0, 0.03); +} + +.magnify-info-panel.theme-milk-white .widget-editor-toggle-slider { + background: rgba(0, 0, 0, 0.12); + border-color: rgba(0, 0, 0, 0.15); +} + +.magnify-info-panel.theme-milk-white .inline-toggle-slider { + background: rgba(0, 0, 0, 0.12); +} + +.magnify-info-panel.theme-milk-white .inline-dropdown-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .inline-dropdown-select:hover { + background-color: rgba(124, 58, 237, 0.08); +} + +.magnify-info-panel.theme-milk-white .inline-dropdown-select option { + background: #fffdfb; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .inline-action-button { + background: var(--info-panel-accent-color, #7c3aed); + color: #ffffff; +} + /* ========================================================================== Inline Controls - Compact controls that render directly in rows ========================================================================== */ diff --git a/src/info-panel/StateManager.ts b/src/info-panel/StateManager.ts index 561e386..31a0e1f 100644 --- a/src/info-panel/StateManager.ts +++ b/src/info-panel/StateManager.ts @@ -148,7 +148,13 @@ export class StateManager { // Extract theme name from various possible formats const paletteStr = String(colorPalette).toLowerCase(); - if (paletteStr.includes('solarized')) { + if (paletteStr.includes('obsidian') && paletteStr.includes('dark')) { + detectedTheme = 'obsidian-dark'; + } else if (paletteStr.includes('obsidian')) { + detectedTheme = 'obsidian'; + } else if (paletteStr.includes('milk') || paletteStr.includes('white')) { + detectedTheme = 'milk-white'; + } else if (paletteStr.includes('solarized')) { detectedTheme = 'solarized'; } else if (paletteStr.includes('arc')) { detectedTheme = 'arc'; @@ -180,7 +186,13 @@ export class StateManager { // Check for theme classes const classes = (body.className + ' ' + html.className).toLowerCase(); - if (classes.includes('theme-solarized') || classes.includes('solarized')) { + if (classes.includes('theme-obsidian-dark') || classes.includes('obsidian-dark')) { + detectedTheme = 'obsidian-dark'; + } else if (classes.includes('theme-obsidian') || classes.includes('obsidian')) { + detectedTheme = 'obsidian'; + } else if (classes.includes('theme-milk-white') || classes.includes('milk-white') || classes.includes('milk_white')) { + detectedTheme = 'milk-white'; + } else if (classes.includes('theme-solarized') || classes.includes('solarized')) { detectedTheme = 'solarized'; } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) { detectedTheme = 'arc'; diff --git a/src/magnify-glass/PopOutManager.ts b/src/magnify-glass/PopOutManager.ts index 1e8b241..a599c3a 100644 --- a/src/magnify-glass/PopOutManager.ts +++ b/src/magnify-glass/PopOutManager.ts @@ -13,7 +13,7 @@ import { WidgetSyncManager } from '../info-panel/widget-editors/WidgetSyncManage * Message types for BroadcastChannel communication */ interface PopOutMessage { - type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node' | 'widget-edit'; + type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node' | 'widget-edit' | 'button-click'; data?: string | Partial | PopOutInfo | NodeListData | number | WidgetEditData; timestamp?: number; } @@ -212,6 +212,10 @@ export class PopOutManager { // Popout is sending a widget value edit this.handleWidgetEdit(message.data as WidgetEditData); break; + case 'button-click': + // Popout is requesting a button widget callback invocation + this.handleButtonClick(message.data as { nodeId: number; widgetName: string }); + break; } } @@ -238,6 +242,41 @@ export class PopOutManager { } } + /** + * Handle button click command from pop-out viewer. + * Invokes the widget's callback function. + */ + private handleButtonClick(buttonData: { nodeId: number; widgetName: string }): void { + if (!buttonData || typeof buttonData.nodeId !== 'number' || !buttonData.widgetName) { + Logger.warn('[PopOut] Invalid button click data:', buttonData); + return; + } + + const app = (window as any).app; + if (!app?.graph) { + Logger.warn('[PopOut] No app.graph available for button click'); + return; + } + + const node = app.graph.getNodeById(buttonData.nodeId); + if (!node?.widgets) { + Logger.warn(`[PopOut] Node ${buttonData.nodeId} not found or has no widgets`); + return; + } + + const widget = node.widgets.find((w: any) => w.name === buttonData.widgetName); + if (widget && typeof widget.callback === 'function') { + try { + widget.callback(widget.value, app.canvas, node, [0, 0], null); + Logger.debug(`[PopOut] Button ${buttonData.widgetName} clicked successfully`); + } catch (error) { + Logger.warn(`[PopOut] Button callback failed:`, error); + } + } else { + Logger.warn(`[PopOut] Widget ${buttonData.widgetName} not found or has no callback`); + } + } + /** * Open the pop-out viewer in a new tab. */ @@ -536,9 +575,24 @@ export class PopOutManager { safeItem.value = item.value; } - // Sanitized options for widgets - if (item.options) { - safeItem.options = this.sanitizeOptions(item.options); + // Handle widget options - preserve arrays for combo dropdowns + if (item.options !== undefined) { + if (Array.isArray(item.options)) { + // For combo widgets, options is an array of values - keep it as is + safeItem.options = item.options.filter( + (opt: any) => typeof opt !== 'function' + ); + } else if (typeof item.options === 'object' && item.options !== null) { + // Check for options.values (ComfyUI nested format for combo widgets) + if (Array.isArray(item.options.values)) { + safeItem.options = item.options.values.filter( + (opt: any) => typeof opt !== 'function' + ); + } else { + // For other widgets, options is an object with config (min, max, step, etc) + safeItem.options = this.sanitizeOptions(item.options); + } + } } return safeItem; diff --git a/tests/unit/UIManager_Dropdown.test.ts b/tests/unit/UIManager_Dropdown.test.ts index ee99e4e..c682611 100644 --- a/tests/unit/UIManager_Dropdown.test.ts +++ b/tests/unit/UIManager_Dropdown.test.ts @@ -82,7 +82,8 @@ describe('UIManager Dropdown Accessibility', () => { it('should cleanup event listeners when item is selected', () => { vi.useFakeTimers(); - const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener'); + const docRemoveSpy = vi.spyOn(document, 'removeEventListener'); + const winRemoveSpy = vi.spyOn(window, 'removeEventListener'); const nodes = [{ id: 1, title: 'Node 1', type: 'Type A' }]; const anchor = document.createElement('div'); @@ -98,14 +99,15 @@ describe('UIManager Dropdown Accessibility', () => { // Simulate click item.click(); - // Assert cleanup was called - expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)); - expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true); + // Assert cleanup was called - keydown is on window, mousedown on document + expect(winRemoveSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true); + expect(docRemoveSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true); vi.useRealTimers(); }); - it('should ignore keyboard events when dropdown is not focused', () => { + it('should focus dropdown on creation for keyboard accessibility', () => { + // Test that dropdown receives focus when created, enabling keyboard navigation vi.useFakeTimers(); const nodes = [{ id: 1, title: 'Node 1', type: 'Type A' }, { id: 2, title: 'Node 2', type: 'Type B' }]; @@ -117,29 +119,22 @@ describe('UIManager Dropdown Accessibility', () => { vi.advanceTimersByTime(100); const dropdown = document.querySelector('.node-selector-dropdown') as HTMLElement; + + // Dropdown should have focus for keyboard accessibility + expect(document.activeElement).toBe(dropdown); + + // First item should be selected by default const items = dropdown.querySelectorAll('.dropdown-item'); - - // Simulate focus moving elsewhere (e.g., user tabs away) - const otherInput = document.createElement('input'); - document.body.appendChild(otherInput); - otherInput.focus(); - expect(document.activeElement).toBe(otherInput); - - // Arrow Down - should NOT change selection if focus is elsewhere - const event = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }); - const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); - - document.dispatchEvent(event); - - expect(preventDefaultSpy).not.toHaveBeenCalled(); - expect(items[1].getAttribute('aria-selected')).toBe('false'); // Should stay on first item + expect(items[0].getAttribute('aria-selected')).toBe('true'); + expect(items[1].getAttribute('aria-selected')).toBe('false'); vi.useRealTimers(); }); it('should cleanup previous event listeners when opening a new dropdown via anchor click', () => { vi.useFakeTimers(); - const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener'); + const docRemoveSpy = vi.spyOn(document, 'removeEventListener'); + const winRemoveSpy = vi.spyOn(window, 'removeEventListener'); const nodes = [{ id: 1, title: 'Node 1', type: 'Type A' }]; const anchor = document.createElement('div'); @@ -149,15 +144,16 @@ describe('UIManager Dropdown Accessibility', () => { (uiManager as any).createDropdown(nodes, anchor, 'title'); vi.advanceTimersByTime(100); - // Reset spy to track calls for the next action - removeEventListenerSpy.mockClear(); + // Reset spies to track calls for the next action + docRemoveSpy.mockClear(); + winRemoveSpy.mockClear(); // Simulate re-opening (which calls hideDropdown then createDropdown) (uiManager as any).showTitleDropdown(anchor); // Verify that cleanup for the FIRST dropdown occurred - expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)); - expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true); + expect(winRemoveSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true); + expect(docRemoveSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true); vi.useRealTimers(); }); diff --git a/web/info-panel.css b/web/info-panel.css index 9c78a95..12ab388 100644 --- a/web/info-panel.css +++ b/web/info-panel.css @@ -59,7 +59,7 @@ } .magnify-info-panel.theme-dark { - background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + background: linear-gradient(135deg, rgb(0, 0, 0), rgb(10, 10, 15)); color: var(--info-panel-text-color, #e0e0e0); border: 1px solid rgba(100, 100, 120, 0.4); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); @@ -67,7 +67,7 @@ } .magnify-info-panel.theme-light { - background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + background: linear-gradient(135deg, rgb(250, 250, 255), rgb(240, 240, 250)); color: var(--info-panel-text-color, #2a2a2a); border: 1px solid rgba(200, 200, 220, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); @@ -76,7 +76,7 @@ /* Solarized Theme - warm, muted tones */ .magnify-info-panel.theme-solarized { - background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96)); + background: linear-gradient(135deg, rgb(0, 43, 54), rgb(7, 54, 66)); color: var(--info-panel-text-color, #839496); border: 1px solid rgba(88, 110, 117, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); @@ -86,7 +86,7 @@ /* Arc Theme - modern blue-gray */ .magnify-info-panel.theme-arc { - background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96)); + background: linear-gradient(135deg, rgb(56, 60, 74), rgb(64, 69, 82)); color: var(--info-panel-text-color, #d3dae3); border: 1px solid rgba(90, 100, 120, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); @@ -96,7 +96,7 @@ /* Nord Theme - cool arctic colors */ .magnify-info-panel.theme-nord { - background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96)); + background: linear-gradient(135deg, rgb(46, 52, 64), rgb(59, 66, 82)); color: var(--info-panel-text-color, #d8dee9); border: 1px solid rgba(76, 86, 106, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); @@ -106,7 +106,7 @@ /* GitHub Theme - clean, professional */ .magnify-info-panel.theme-github { - background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96)); + background: linear-gradient(135deg, rgb(255, 255, 255), rgb(246, 248, 250)); color: var(--info-panel-text-color, #24292e); border: 1px solid rgba(225, 228, 232, 0.8); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); @@ -114,6 +114,37 @@ --info-panel-accent-color: #0366d6; } +/* Obsidian Theme - deep purple/charcoal */ +.magnify-info-panel.theme-obsidian { + background: linear-gradient(135deg, rgb(30, 30, 40), rgb(40, 40, 55)); + color: var(--info-panel-text-color, #d4d4d8); + border: 1px solid rgba(80, 80, 100, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + --info-panel-accent-color: #8b5cf6; +} + +/* Obsidian Dark Theme - even darker obsidian */ +.magnify-info-panel.theme-obsidian-dark { + background: linear-gradient(135deg, rgb(15, 15, 20), rgb(25, 25, 35)); + color: var(--info-panel-text-color, #c4c4cc); + border: 1px solid rgba(60, 60, 80, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 16px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(12px); + --info-panel-accent-color: #a78bfa; +} + +/* Milk White Theme - soft cream/white */ +.magnify-info-panel.theme-milk-white { + background: linear-gradient(135deg, rgb(255, 253, 250), rgb(250, 248, 245)); + color: var(--info-panel-text-color, #3d3d3d); + border: 1px solid rgba(220, 215, 210, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 16px rgba(0, 0, 0, 0.04); + backdrop-filter: blur(12px); + --info-panel-accent-color: #7c3aed; +} + + .magnify-info-panel.visible { transform: translateY(0) !important; } @@ -1265,6 +1296,68 @@ color: var(--info-panel-accent-color, #0366d6); } +/* Milk White Theme - light editor adjustments */ +.magnify-info-panel.theme-milk-white .widget-editor-input, +.magnify-info-panel.theme-milk-white .widget-editor-textarea { + background: rgba(255, 255, 255, 0.8); + border-color: rgba(180, 175, 170, 0.4); + color: #3d3d3d; +} + +.magnify-info-panel.theme-milk-white .widget-editor-input:focus, +.magnify-info-panel.theme-milk-white .widget-editor-textarea:focus { + border-color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .widget-editor-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .widget-editor-select:hover { + background-color: rgba(124, 58, 237, 0.08); +} + +.magnify-info-panel.theme-milk-white .widget-editor-select option { + background: #fffdfb; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .widget-editor-stepper { + border-color: rgba(0, 0, 0, 0.12); + background: rgba(0, 0, 0, 0.03); +} + +.magnify-info-panel.theme-milk-white .widget-editor-toggle-slider { + background: rgba(0, 0, 0, 0.12); + border-color: rgba(0, 0, 0, 0.15); +} + +.magnify-info-panel.theme-milk-white .inline-toggle-slider { + background: rgba(0, 0, 0, 0.12); +} + +.magnify-info-panel.theme-milk-white .inline-dropdown-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .inline-dropdown-select:hover { + background-color: rgba(124, 58, 237, 0.08); +} + +.magnify-info-panel.theme-milk-white .inline-dropdown-select option { + background: #fffdfb; + color: var(--info-panel-accent-color, #7c3aed); +} + +.magnify-info-panel.theme-milk-white .inline-action-button { + background: var(--info-panel-accent-color, #7c3aed); + color: #ffffff; +} + /* ========================================================================== Inline Controls - Compact controls that render directly in rows ========================================================================== */ diff --git a/web/info-panel/StateManager.js b/web/info-panel/StateManager.js index d347387..5591d62 100644 --- a/web/info-panel/StateManager.js +++ b/web/info-panel/StateManager.js @@ -71,7 +71,13 @@ class StateManager { const colorPalette = getSettingValue("Comfy.ColorPalette", ""); if (colorPalette) { const paletteStr = String(colorPalette).toLowerCase(); - if (paletteStr.includes("solarized")) { + if (paletteStr.includes("obsidian") && paletteStr.includes("dark")) { + detectedTheme = "obsidian-dark"; + } else if (paletteStr.includes("obsidian")) { + detectedTheme = "obsidian"; + } else if (paletteStr.includes("milk") || paletteStr.includes("white")) { + detectedTheme = "milk-white"; + } else if (paletteStr.includes("solarized")) { detectedTheme = "solarized"; } else if (paletteStr.includes("arc")) { detectedTheme = "arc"; @@ -96,7 +102,13 @@ class StateManager { detectedTheme = dataTheme.toLowerCase(); } const classes = (body.className + " " + html.className).toLowerCase(); - if (classes.includes("theme-solarized") || classes.includes("solarized")) { + if (classes.includes("theme-obsidian-dark") || classes.includes("obsidian-dark")) { + detectedTheme = "obsidian-dark"; + } else if (classes.includes("theme-obsidian") || classes.includes("obsidian")) { + detectedTheme = "obsidian"; + } else if (classes.includes("theme-milk-white") || classes.includes("milk-white") || classes.includes("milk_white")) { + detectedTheme = "milk-white"; + } else if (classes.includes("theme-solarized") || classes.includes("solarized")) { detectedTheme = "solarized"; } else if (classes.includes("theme-arc") || classes.includes("arc-theme")) { detectedTheme = "arc"; diff --git a/web/info-panel/StateManager.js.map b/web/info-panel/StateManager.js.map index 00def3a..9970987 100644 --- a/web/info-panel/StateManager.js.map +++ b/web/info-panel/StateManager.js.map @@ -1 +1 @@ -{"version":3,"file":"StateManager.js","sources":["../../src/info-panel/StateManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript)\n * \n * Centralized state management for the information panel.\n */\n\nimport type { GatheredInfo, SettingsChange } from '../types/comfyui';\nimport { getSettingValue, clamp } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\n isPanelMinimized: boolean;\n isPanelPinned: boolean;\n isPanelLocked: boolean; // New lock state\n isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: number; y: number };\n lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: boolean;\n\n // Info freezing\n isInfoHeld: boolean;\n\n // Section expansion\n expandedSections: Set;\n\n // Interaction states\n isPanelHovered: boolean;\n isHoveringNode: boolean;\n\n // Auto-collapse\n autoExpandTimer: ReturnType | null;\n lastNodeId: string | number | null;\n\n // Update scheduling\n updateScheduled: boolean;\n isInitialLoading: boolean;\n\n // Current data\n currentInfo: GatheredInfo | null;\n\n // Node selection override (user-selected node from dropdown)\n selectedNodeId: number | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\n\n // Auto-detected theme\n currentTheme: string;\n}\n\n/**\n * State Manager class.\n * Handles all state-related operations and provides clean state transitions.\n */\nexport class StateManager {\n state: InfoPanelStateData;\n\n constructor() {\n this.state = {\n // Panel visibility and positioning\n isPanelVisible: false,\n wasPanelVisibleBeforeHide: false,\n isPanelMinimized: false,\n isPanelPinned: false,\n isPanelLocked: false, // New lock state\n isAutoPinned: false, // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: 0, y: 0 },\n lastPinnedPosition: null, // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: true,\n isInfoHeld: false,\n\n // Section expansion\n expandedSections: new Set(['node']),\n\n // Interaction states\n isPanelHovered: false,\n isHoveringNode: false,\n\n // Auto-collapse\n autoExpandTimer: null,\n lastNodeId: null,\n\n // Update scheduling\n updateScheduled: false,\n isInitialLoading: false,\n\n // Current data\n currentInfo: null,\n\n // Node selection override\n selectedNodeId: null,\n\n // Settings cache\n settings: {},\n\n // Auto-detected theme\n currentTheme: 'dark' // Will be auto-detected\n };\n\n this.initThemeDetection();\n this.loadSettings();\n }\n\n initThemeDetection(): void {\n // Detect ComfyUI's current theme\n this.detectCurrentTheme();\n\n // Force an immediate check in case we missed the initial state\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 1000);\n\n // Set up mutation observer to watch for theme changes\n this.setupThemeObserver();\n\n // Listen for system theme changes as fallback\n if (window.matchMedia) {\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n if (prefersDark.addListener) {\n prefersDark.addListener(() => this.detectCurrentTheme());\n } else if (prefersDark.addEventListener) {\n prefersDark.addEventListener('change', () => this.detectCurrentTheme());\n }\n }\n\n // Add click listener to theme toggle buttons for immediate detection\n this.setupThemeButtonListeners();\n }\n\n detectCurrentTheme(): boolean {\n let detectedTheme = 'dark'; // Default fallback\n\n // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n } else if (classes.includes('theme-dracula') || classes.includes('dracula-theme')) {\n detectedTheme = 'dracula';\n } else if (classes.includes('theme-gruvbox') || classes.includes('gruvbox-theme')) {\n detectedTheme = 'gruvbox';\n } else if (classes.includes('theme-monokai') || classes.includes('monokai-theme')) {\n detectedTheme = 'monokai';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\n if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {\n const rgbMatch = bgColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 180) {\n detectedTheme = 'light';\n }\n }\n }\n }\n\n // Update theme if it changed\n if (this.state.currentTheme !== detectedTheme) {\n this.state.currentTheme = detectedTheme;\n return true; // Theme changed\n }\n\n return false; // No change\n }\n\n setupThemeObserver(): void {\n // Watch for changes to body/html classes and attributes\n const observer = new MutationObserver((mutations) => {\n let shouldCheck = false;\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'class' ||\n mutation.attributeName === 'data-theme' ||\n mutation.attributeName === 'style')) {\n shouldCheck = true;\n }\n });\n\n if (shouldCheck && this.detectCurrentTheme()) {\n // Theme changed, update UI immediately\n this.notifyThemeChange();\n }\n });\n\n // Observe changes to body, html, and vue app\n observer.observe(document.body, { attributes: true, attributeOldValue: true });\n observer.observe(document.documentElement, { attributes: true, attributeOldValue: true });\n\n const vueApp = document.querySelector('#vue-app');\n if (vueApp) {\n observer.observe(vueApp, { attributes: true, attributeOldValue: true });\n }\n\n // Also watch for changes to any element with theme-related classes\n const themeElements = document.querySelectorAll('[class*=\"theme\"], [class*=\"dark\"], [class*=\"light\"], [data-theme]');\n themeElements.forEach(el => {\n observer.observe(el, { attributes: true, attributeOldValue: true });\n });\n\n // Periodic fallback check every 2 seconds\n setInterval(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 2000);\n }\n\n setupThemeButtonListeners(): void {\n // Listen for clicks on theme toggle buttons\n document.addEventListener('click', (e) => {\n // Check if clicked element might be a theme toggle\n const target = e.target as HTMLElement;\n const isThemeButton = target.textContent?.toLowerCase().includes('theme') ||\n target.textContent?.toLowerCase().includes('light') ||\n target.textContent?.toLowerCase().includes('dark') ||\n target.title?.toLowerCase().includes('theme') ||\n (typeof target.className === 'string' && target.className.toLowerCase().includes('theme'));\n\n if (isThemeButton) {\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 100);\n }\n });\n }\n\n notifyThemeChange(): void {\n // Update settings to reflect new theme\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n\n // Notify UI components of theme change immediately\n if (window.infoPanelManager && window.infoPanelManager.uiManager) {\n window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme);\n }\n\n // Notify Pop-Out Manager (if it exists on the main glass instance)\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.popOutManager) {\n magnifyGlass.popOutManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n const defaults = DEFAULT_PANEL_SETTINGS;\n\n Object.keys(defaults).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (defaults as any)[key]);\n }\n });\n\n // Validate numeric settings\n this.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"], 200, 600, defaults[\"🔍MagnifyGlass.InfoPanelWidth\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"], 200, 1500, defaults[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"], 10, 100, defaults[\"🔍MagnifyGlass.InfoPanelOpacity\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontSize\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontSize\"], 8, 24, defaults[\"🔍MagnifyGlass.InfoPanelFontSize\"] as number\n );\n\n // Validate colors\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] = this.validateColor(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"], defaults[\"🔍MagnifyGlass.InfoPanelTextColor\"] as string\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] = this.validateColor(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"], defaults[\"🔍MagnifyGlass.InfoPanelAccentColor\"] as string\n );\n\n // Validate Enums\n const validPanelPositions = ['Bottom', 'Top', 'Left', 'Right'];\n this.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"], validPanelPositions, defaults[\"🔍MagnifyGlass.InfoPanelPosition\"] as string\n );\n\n const validControlPositions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top', 'bottom', 'left', 'right'];\n this.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.ControlsPosition\"], validControlPositions, defaults[\"🔍MagnifyGlass.ControlsPosition\"] as string\n );\n\n const validFonts = [\n 'System Default', 'Inter', 'Roboto', 'JetBrains Mono', 'Fira Code',\n 'IBM Plex Sans', 'Space Grotesk', 'Lexend', 'Outfit', 'monospace', 'system-ui'\n ];\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"], validFonts, defaults[\"🔍MagnifyGlass.InfoPanelFontFamily\"] as string\n );\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n /**\n * Validate numeric input with bounds checking.\n */\n private validateNumber(value: unknown, min: number, max: number, fallback: number): number {\n const num = Number(value);\n if (isNaN(num)) return fallback;\n return clamp(num, min, max);\n }\n\n /**\n * Validate color string (hex).\n */\n private validateColor(color: unknown, fallback: string): string {\n if (!color || typeof color !== 'string') return fallback;\n // Basic hex validation: #RGB, #RRGGBB, #RRGGBBAA\n if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(color)) {\n return color;\n }\n return fallback;\n }\n\n /**\n * Validate string against an allowlist.\n */\n private validateStringOption(value: unknown, options: string[], fallback: string): string {\n if (typeof value !== 'string') return fallback;\n return options.includes(value) ? value : fallback;\n }\n\n updateSettings(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\n Object.keys(this.state.settings).forEach(key => {\n if (oldSettings[key] !== this.state.settings[key]) {\n changes[key] = {\n old: oldSettings[key],\n new: this.state.settings[key]\n };\n }\n });\n\n return changes;\n }\n\n togglePanelVisibility(): boolean {\n this.state.isPanelVisible = !this.state.isPanelVisible;\n return this.state.isPanelVisible;\n }\n\n toggleGlassPreview(): boolean {\n this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible;\n return this.state.isGlassPreviewVisible;\n }\n\n togglePinning(): boolean {\n const wasPinned = this.state.isPanelPinned;\n this.state.isPanelPinned = !this.state.isPanelPinned;\n\n // When unpinning, also unlock and clear auto-pin flag\n if (!this.state.isPanelPinned) {\n this.state.isPanelLocked = false;\n this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning\n } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\n }\n\n if (this.state.isPanelPinned && this.state.lastPinnedPosition) {\n // Restore to last pinned position\n this.state.pinnedPosition = { ...this.state.lastPinnedPosition };\n } else if (!this.state.isPanelPinned && wasPinned) {\n // Save current position when unpinning\n this.state.lastPinnedPosition = { ...this.state.pinnedPosition };\n }\n\n return this.state.isPanelPinned;\n }\n\n toggleLocking(): boolean {\n // Only allow locking when pinned\n if (!this.state.isPanelPinned) {\n return false;\n }\n\n this.state.isPanelLocked = !this.state.isPanelLocked;\n return this.state.isPanelLocked;\n }\n\n toggleMinimized(): boolean {\n this.state.isPanelMinimized = !this.state.isPanelMinimized;\n return this.state.isPanelMinimized;\n }\n\n toggleHold(): boolean {\n this.state.isInfoHeld = !this.state.isInfoHeld;\n return this.state.isInfoHeld;\n }\n\n toggleSection(sectionId: string): boolean {\n if (sectionId === 'node') return false; // Node section always expanded\n\n if (this.state.expandedSections.has(sectionId)) {\n this.state.expandedSections.delete(sectionId);\n } else {\n this.state.expandedSections.add(sectionId);\n }\n return true;\n }\n\n setPinnedPosition(x: number, y: number): void {\n this.state.pinnedPosition = { x, y };\n // Also save as last pinned position for memory\n this.state.lastPinnedPosition = { x, y };\n }\n\n setCurrentInfo(info: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n /**\n * Set the selected node ID for manual override.\n * When set, the inspector will display this node instead of the hovered node.\n */\n setSelectedNode(id: number | null): void {\n this.state.selectedNodeId = id;\n }\n\n /**\n * Clear the selected node, returning to hover-based detection.\n */\n clearSelectedNode(): void {\n this.state.selectedNodeId = null;\n }\n\n scheduleAutoCollapse(): void {\n this.clearAutoExpandTimer();\n if (this.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]) {\n this.state.autoExpandTimer = setTimeout(() => {\n if (!this.state.isPanelHovered) {\n this.collapseNodeSections();\n }\n }, 1500);\n }\n }\n\n clearAutoExpandTimer(): void {\n if (this.state.autoExpandTimer) {\n clearTimeout(this.state.autoExpandTimer);\n this.state.autoExpandTimer = null;\n }\n }\n\n expandNodeSections(): void {\n this.state.expandedSections.add('hoveredNode');\n this.state.expandedSections.add('node');\n this.state.expandedSections.add('cursor');\n this.state.expandedSections.add('canvas');\n this.state.expandedSections.add('magnifier');\n this.clearAutoExpandTimer();\n }\n\n collapseNodeSections(): void {\n this.state.expandedSections.delete('hoveredNode');\n this.state.expandedSections.delete('node');\n this.state.expandedSections.delete('widget');\n }\n\n cleanup(): void {\n this.clearAutoExpandTimer();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"StateManager.js","sources":["../../src/info-panel/StateManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript)\n * \n * Centralized state management for the information panel.\n */\n\nimport type { GatheredInfo, SettingsChange } from '../types/comfyui';\nimport { getSettingValue, clamp } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\n isPanelMinimized: boolean;\n isPanelPinned: boolean;\n isPanelLocked: boolean; // New lock state\n isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: number; y: number };\n lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: boolean;\n\n // Info freezing\n isInfoHeld: boolean;\n\n // Section expansion\n expandedSections: Set;\n\n // Interaction states\n isPanelHovered: boolean;\n isHoveringNode: boolean;\n\n // Auto-collapse\n autoExpandTimer: ReturnType | null;\n lastNodeId: string | number | null;\n\n // Update scheduling\n updateScheduled: boolean;\n isInitialLoading: boolean;\n\n // Current data\n currentInfo: GatheredInfo | null;\n\n // Node selection override (user-selected node from dropdown)\n selectedNodeId: number | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\n\n // Auto-detected theme\n currentTheme: string;\n}\n\n/**\n * State Manager class.\n * Handles all state-related operations and provides clean state transitions.\n */\nexport class StateManager {\n state: InfoPanelStateData;\n\n constructor() {\n this.state = {\n // Panel visibility and positioning\n isPanelVisible: false,\n wasPanelVisibleBeforeHide: false,\n isPanelMinimized: false,\n isPanelPinned: false,\n isPanelLocked: false, // New lock state\n isAutoPinned: false, // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: 0, y: 0 },\n lastPinnedPosition: null, // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: true,\n isInfoHeld: false,\n\n // Section expansion\n expandedSections: new Set(['node']),\n\n // Interaction states\n isPanelHovered: false,\n isHoveringNode: false,\n\n // Auto-collapse\n autoExpandTimer: null,\n lastNodeId: null,\n\n // Update scheduling\n updateScheduled: false,\n isInitialLoading: false,\n\n // Current data\n currentInfo: null,\n\n // Node selection override\n selectedNodeId: null,\n\n // Settings cache\n settings: {},\n\n // Auto-detected theme\n currentTheme: 'dark' // Will be auto-detected\n };\n\n this.initThemeDetection();\n this.loadSettings();\n }\n\n initThemeDetection(): void {\n // Detect ComfyUI's current theme\n this.detectCurrentTheme();\n\n // Force an immediate check in case we missed the initial state\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 1000);\n\n // Set up mutation observer to watch for theme changes\n this.setupThemeObserver();\n\n // Listen for system theme changes as fallback\n if (window.matchMedia) {\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n if (prefersDark.addListener) {\n prefersDark.addListener(() => this.detectCurrentTheme());\n } else if (prefersDark.addEventListener) {\n prefersDark.addEventListener('change', () => this.detectCurrentTheme());\n }\n }\n\n // Add click listener to theme toggle buttons for immediate detection\n this.setupThemeButtonListeners();\n }\n\n detectCurrentTheme(): boolean {\n let detectedTheme = 'dark'; // Default fallback\n\n // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('obsidian') && paletteStr.includes('dark')) {\n detectedTheme = 'obsidian-dark';\n } else if (paletteStr.includes('obsidian')) {\n detectedTheme = 'obsidian';\n } else if (paletteStr.includes('milk') || paletteStr.includes('white')) {\n detectedTheme = 'milk-white';\n } else if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-obsidian-dark') || classes.includes('obsidian-dark')) {\n detectedTheme = 'obsidian-dark';\n } else if (classes.includes('theme-obsidian') || classes.includes('obsidian')) {\n detectedTheme = 'obsidian';\n } else if (classes.includes('theme-milk-white') || classes.includes('milk-white') || classes.includes('milk_white')) {\n detectedTheme = 'milk-white';\n } else if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n } else if (classes.includes('theme-dracula') || classes.includes('dracula-theme')) {\n detectedTheme = 'dracula';\n } else if (classes.includes('theme-gruvbox') || classes.includes('gruvbox-theme')) {\n detectedTheme = 'gruvbox';\n } else if (classes.includes('theme-monokai') || classes.includes('monokai-theme')) {\n detectedTheme = 'monokai';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\n if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {\n const rgbMatch = bgColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 180) {\n detectedTheme = 'light';\n }\n }\n }\n }\n\n // Update theme if it changed\n if (this.state.currentTheme !== detectedTheme) {\n this.state.currentTheme = detectedTheme;\n return true; // Theme changed\n }\n\n return false; // No change\n }\n\n setupThemeObserver(): void {\n // Watch for changes to body/html classes and attributes\n const observer = new MutationObserver((mutations) => {\n let shouldCheck = false;\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'class' ||\n mutation.attributeName === 'data-theme' ||\n mutation.attributeName === 'style')) {\n shouldCheck = true;\n }\n });\n\n if (shouldCheck && this.detectCurrentTheme()) {\n // Theme changed, update UI immediately\n this.notifyThemeChange();\n }\n });\n\n // Observe changes to body, html, and vue app\n observer.observe(document.body, { attributes: true, attributeOldValue: true });\n observer.observe(document.documentElement, { attributes: true, attributeOldValue: true });\n\n const vueApp = document.querySelector('#vue-app');\n if (vueApp) {\n observer.observe(vueApp, { attributes: true, attributeOldValue: true });\n }\n\n // Also watch for changes to any element with theme-related classes\n const themeElements = document.querySelectorAll('[class*=\"theme\"], [class*=\"dark\"], [class*=\"light\"], [data-theme]');\n themeElements.forEach(el => {\n observer.observe(el, { attributes: true, attributeOldValue: true });\n });\n\n // Periodic fallback check every 2 seconds\n setInterval(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 2000);\n }\n\n setupThemeButtonListeners(): void {\n // Listen for clicks on theme toggle buttons\n document.addEventListener('click', (e) => {\n // Check if clicked element might be a theme toggle\n const target = e.target as HTMLElement;\n const isThemeButton = target.textContent?.toLowerCase().includes('theme') ||\n target.textContent?.toLowerCase().includes('light') ||\n target.textContent?.toLowerCase().includes('dark') ||\n target.title?.toLowerCase().includes('theme') ||\n (typeof target.className === 'string' && target.className.toLowerCase().includes('theme'));\n\n if (isThemeButton) {\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 100);\n }\n });\n }\n\n notifyThemeChange(): void {\n // Update settings to reflect new theme\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n\n // Notify UI components of theme change immediately\n if (window.infoPanelManager && window.infoPanelManager.uiManager) {\n window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme);\n }\n\n // Notify Pop-Out Manager (if it exists on the main glass instance)\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.popOutManager) {\n magnifyGlass.popOutManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n const defaults = DEFAULT_PANEL_SETTINGS;\n\n Object.keys(defaults).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (defaults as any)[key]);\n }\n });\n\n // Validate numeric settings\n this.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"], 200, 600, defaults[\"🔍MagnifyGlass.InfoPanelWidth\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"], 200, 1500, defaults[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"], 10, 100, defaults[\"🔍MagnifyGlass.InfoPanelOpacity\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontSize\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontSize\"], 8, 24, defaults[\"🔍MagnifyGlass.InfoPanelFontSize\"] as number\n );\n\n // Validate colors\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] = this.validateColor(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"], defaults[\"🔍MagnifyGlass.InfoPanelTextColor\"] as string\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] = this.validateColor(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"], defaults[\"🔍MagnifyGlass.InfoPanelAccentColor\"] as string\n );\n\n // Validate Enums\n const validPanelPositions = ['Bottom', 'Top', 'Left', 'Right'];\n this.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"], validPanelPositions, defaults[\"🔍MagnifyGlass.InfoPanelPosition\"] as string\n );\n\n const validControlPositions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top', 'bottom', 'left', 'right'];\n this.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.ControlsPosition\"], validControlPositions, defaults[\"🔍MagnifyGlass.ControlsPosition\"] as string\n );\n\n const validFonts = [\n 'System Default', 'Inter', 'Roboto', 'JetBrains Mono', 'Fira Code',\n 'IBM Plex Sans', 'Space Grotesk', 'Lexend', 'Outfit', 'monospace', 'system-ui'\n ];\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"], validFonts, defaults[\"🔍MagnifyGlass.InfoPanelFontFamily\"] as string\n );\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n /**\n * Validate numeric input with bounds checking.\n */\n private validateNumber(value: unknown, min: number, max: number, fallback: number): number {\n const num = Number(value);\n if (isNaN(num)) return fallback;\n return clamp(num, min, max);\n }\n\n /**\n * Validate color string (hex).\n */\n private validateColor(color: unknown, fallback: string): string {\n if (!color || typeof color !== 'string') return fallback;\n // Basic hex validation: #RGB, #RRGGBB, #RRGGBBAA\n if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(color)) {\n return color;\n }\n return fallback;\n }\n\n /**\n * Validate string against an allowlist.\n */\n private validateStringOption(value: unknown, options: string[], fallback: string): string {\n if (typeof value !== 'string') return fallback;\n return options.includes(value) ? value : fallback;\n }\n\n updateSettings(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\n Object.keys(this.state.settings).forEach(key => {\n if (oldSettings[key] !== this.state.settings[key]) {\n changes[key] = {\n old: oldSettings[key],\n new: this.state.settings[key]\n };\n }\n });\n\n return changes;\n }\n\n togglePanelVisibility(): boolean {\n this.state.isPanelVisible = !this.state.isPanelVisible;\n return this.state.isPanelVisible;\n }\n\n toggleGlassPreview(): boolean {\n this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible;\n return this.state.isGlassPreviewVisible;\n }\n\n togglePinning(): boolean {\n const wasPinned = this.state.isPanelPinned;\n this.state.isPanelPinned = !this.state.isPanelPinned;\n\n // When unpinning, also unlock and clear auto-pin flag\n if (!this.state.isPanelPinned) {\n this.state.isPanelLocked = false;\n this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning\n } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\n }\n\n if (this.state.isPanelPinned && this.state.lastPinnedPosition) {\n // Restore to last pinned position\n this.state.pinnedPosition = { ...this.state.lastPinnedPosition };\n } else if (!this.state.isPanelPinned && wasPinned) {\n // Save current position when unpinning\n this.state.lastPinnedPosition = { ...this.state.pinnedPosition };\n }\n\n return this.state.isPanelPinned;\n }\n\n toggleLocking(): boolean {\n // Only allow locking when pinned\n if (!this.state.isPanelPinned) {\n return false;\n }\n\n this.state.isPanelLocked = !this.state.isPanelLocked;\n return this.state.isPanelLocked;\n }\n\n toggleMinimized(): boolean {\n this.state.isPanelMinimized = !this.state.isPanelMinimized;\n return this.state.isPanelMinimized;\n }\n\n toggleHold(): boolean {\n this.state.isInfoHeld = !this.state.isInfoHeld;\n return this.state.isInfoHeld;\n }\n\n toggleSection(sectionId: string): boolean {\n if (sectionId === 'node') return false; // Node section always expanded\n\n if (this.state.expandedSections.has(sectionId)) {\n this.state.expandedSections.delete(sectionId);\n } else {\n this.state.expandedSections.add(sectionId);\n }\n return true;\n }\n\n setPinnedPosition(x: number, y: number): void {\n this.state.pinnedPosition = { x, y };\n // Also save as last pinned position for memory\n this.state.lastPinnedPosition = { x, y };\n }\n\n setCurrentInfo(info: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n /**\n * Set the selected node ID for manual override.\n * When set, the inspector will display this node instead of the hovered node.\n */\n setSelectedNode(id: number | null): void {\n this.state.selectedNodeId = id;\n }\n\n /**\n * Clear the selected node, returning to hover-based detection.\n */\n clearSelectedNode(): void {\n this.state.selectedNodeId = null;\n }\n\n scheduleAutoCollapse(): void {\n this.clearAutoExpandTimer();\n if (this.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]) {\n this.state.autoExpandTimer = setTimeout(() => {\n if (!this.state.isPanelHovered) {\n this.collapseNodeSections();\n }\n }, 1500);\n }\n }\n\n clearAutoExpandTimer(): void {\n if (this.state.autoExpandTimer) {\n clearTimeout(this.state.autoExpandTimer);\n this.state.autoExpandTimer = null;\n }\n }\n\n expandNodeSections(): void {\n this.state.expandedSections.add('hoveredNode');\n this.state.expandedSections.add('node');\n this.state.expandedSections.add('cursor');\n this.state.expandedSections.add('canvas');\n this.state.expandedSections.add('magnifier');\n this.clearAutoExpandTimer();\n }\n\n collapseNodeSections(): void {\n this.state.expandedSections.delete('hoveredNode');\n this.state.expandedSections.delete('node');\n this.state.expandedSections.delete('widget');\n }\n\n cleanup(): void {\n this.clearAutoExpandTimer();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/PopOutManager.js b/web/magnify-glass/PopOutManager.js index 64679be..ec40e2a 100644 --- a/web/magnify-glass/PopOutManager.js +++ b/web/magnify-glass/PopOutManager.js @@ -110,6 +110,9 @@ class PopOutManager { case "widget-edit": this.handleWidgetEdit(message.data); break; + case "button-click": + this.handleButtonClick(message.data); + break; } } /** @@ -132,6 +135,37 @@ class PopOutManager { Logger.warn(`[PopOut] Widget edit failed: ${result.error}`); } } + /** + * Handle button click command from pop-out viewer. + * Invokes the widget's callback function. + */ + handleButtonClick(buttonData) { + if (!buttonData || typeof buttonData.nodeId !== "number" || !buttonData.widgetName) { + Logger.warn("[PopOut] Invalid button click data:", buttonData); + return; + } + const app = window.app; + if (!(app == null ? void 0 : app.graph)) { + Logger.warn("[PopOut] No app.graph available for button click"); + return; + } + const node = app.graph.getNodeById(buttonData.nodeId); + if (!(node == null ? void 0 : node.widgets)) { + Logger.warn(`[PopOut] Node ${buttonData.nodeId} not found or has no widgets`); + return; + } + const widget = node.widgets.find((w) => w.name === buttonData.widgetName); + if (widget && typeof widget.callback === "function") { + try { + widget.callback(widget.value, app.canvas, node, [0, 0], null); + Logger.debug(`[PopOut] Button ${buttonData.widgetName} clicked successfully`); + } catch (error) { + Logger.warn(`[PopOut] Button callback failed:`, error); + } + } else { + Logger.warn(`[PopOut] Widget ${buttonData.widgetName} not found or has no callback`); + } + } /** * Open the pop-out viewer in a new tab. */ @@ -360,8 +394,20 @@ class PopOutManager { if (item.value !== void 0 && typeof item.value !== "function") { safeItem.value = item.value; } - if (item.options) { - safeItem.options = this.sanitizeOptions(item.options); + if (item.options !== void 0) { + if (Array.isArray(item.options)) { + safeItem.options = item.options.filter( + (opt) => typeof opt !== "function" + ); + } else if (typeof item.options === "object" && item.options !== null) { + if (Array.isArray(item.options.values)) { + safeItem.options = item.options.values.filter( + (opt) => typeof opt !== "function" + ); + } else { + safeItem.options = this.sanitizeOptions(item.options); + } + } } return safeItem; }); diff --git a/web/magnify-glass/PopOutManager.js.map b/web/magnify-glass/PopOutManager.js.map index c9783a1..f7242c2 100644 --- a/web/magnify-glass/PopOutManager.js.map +++ b/web/magnify-glass/PopOutManager.js.map @@ -1 +1 @@ -{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\nimport { WidgetSyncManager } from '../info-panel/widget-editors/WidgetSyncManager';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node' | 'widget-edit';\n data?: string | Partial | PopOutInfo | NodeListData | number | WidgetEditData;\n timestamp?: number;\n}\n\n/**\n * Widget edit command data from pop-out viewer\n */\ninterface WidgetEditData {\n nodeId: number;\n widgetName: string;\n value: unknown;\n}\n\n/**\n * Node list data sent to popout for dropdown\n */\ninterface NodeListData {\n nodes: { id: number; title: string; type: string; order?: number }[];\n sortBy: 'title' | 'execOrder' | 'id';\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n theme?: string;\n}\n\n/**\n * Inspector info data structure for pop-out viewer\n */\ninterface PopOutInfo {\n hoveredNode?: {\n title: string;\n type: string;\n id?: number;\n mode?: string | number;\n pos?: { x: number; y: number } | string;\n size?: { w: number; h: number } | string;\n executionOrder?: number;\n category?: string;\n pythonModule?: string;\n widgets?: any[];\n inputs?: any[];\n outputs?: any[];\n properties?: Record;\n };\n cursor?: {\n canvas?: { x: number; y: number };\n };\n canvas?: {\n scale: number;\n };\n magnifier?: {\n zoomFactor: number;\n };\n media?: {\n tagName: string;\n naturalSize?: string;\n };\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => void) | null = null;\n public onNodeSelect: ((nodeId: number) => void) | null = null;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n private currentTheme: string = 'dark'; // Default theme\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Cache-busting version - increment to force refresh\n const version = 'v25';\n\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);\n\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n Logger.debug(`[PopOut] First script src: ${src}`);\n\n // src might be like: /extensions/comfyui-magnifyglass/magnify-glass/MagnifyGlass.js\n // We need to get to the web root: /extensions/comfyui-magnifyglass/\n const urlParts = src.split('/');\n // Find the extension folder (contains 'magnify' in name)\n let extensionIndex = -1;\n for (let i = 0; i < urlParts.length; i++) {\n if (urlParts[i].toLowerCase().includes('magnify')) {\n extensionIndex = i;\n break;\n }\n }\n if (extensionIndex >= 0) {\n const baseUrl = urlParts.slice(0, extensionIndex + 1).join('/');\n const viewerUrl = `${baseUrl}/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using viewer URL: ${viewerUrl}`);\n return viewerUrl;\n }\n }\n // Fallback: assume standard ComfyUI extension path\n const fallbackUrl = `/extensions/comfyui-magnifyglass/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using fallback URL: ${fallbackUrl}`);\n return fallbackUrl;\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n // Start pinging immediately to detect if viewer is already open (e.g. after reload)\n this.startPing();\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n if (this.onStateChange) this.onStateChange(true);\n // Send current theme immediately upon connection\n this.sendConfig({ theme: this.currentTheme });\n }\n break;\n case 'close':\n this.isOpen = false;\n this.popOutWindow = null;\n // Don't stop pinging, we might want to reconnect if user re-opens externally\n // or if we just want to keep checking. \n // However, original logic stopped ping. Let's keep pinging to auto-detect return.\n // this.stopPing(); \n Logger.debug('[PopOut] Viewer tab closed');\n if (this.onStateChange) this.onStateChange(false);\n break;\n case 'request-nodes':\n // Popout is requesting the node list for dropdown\n Logger.debug('[PopOut] Received request-nodes:', message.data);\n this.handleNodeListRequest(message.data as 'title' | 'execOrder' | 'id');\n break;\n case 'node-select':\n // Popout selected a node, notify main window\n if (this.onNodeSelect && typeof message.data === 'number') {\n this.onNodeSelect(message.data);\n }\n break;\n case 'zoom-node':\n // Popout requested to zoom to a node\n if (typeof message.data === 'number') {\n const app = (window as any).app;\n const node = app.graph.getNodeById(message.data);\n if (node && app.canvas) {\n app.canvas.centerOnNode(node);\n }\n }\n break;\n case 'widget-edit':\n // Popout is sending a widget value edit\n this.handleWidgetEdit(message.data as WidgetEditData);\n break;\n }\n }\n\n /**\n * Handle widget edit command from pop-out viewer.\n * Syncs the edit to the actual node widget on the canvas.\n */\n private handleWidgetEdit(editData: WidgetEditData): void {\n if (!editData || typeof editData.nodeId !== 'number' || !editData.widgetName) {\n Logger.warn('[PopOut] Invalid widget edit data:', editData);\n return;\n }\n\n const result = WidgetSyncManager.syncWidgetValue(\n editData.nodeId,\n editData.widgetName,\n editData.value\n );\n\n if (result.success) {\n Logger.debug(`[PopOut] Widget edit synced: ${editData.widgetName} = ${editData.value}`);\n } else {\n Logger.warn(`[PopOut] Widget edit failed: ${result.error}`);\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n // If window exists and is open, focus it\n if (this.popOutWindow && !this.popOutWindow.closed) {\n this.popOutWindow.focus();\n return;\n }\n\n // Open new tab (or refocus existing if name matches)\n // Use a constant name to ensure singleton behavior across reloads\n this.popOutWindow = window.open(this.viewerUrl, 'MagnifyGlassPopout');\n\n if (!this.popOutWindow) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen && !this.popOutWindow) return;\n\n // update state immediately for instant UI feedback\n this.isOpen = false;\n if (this.onStateChange) this.onStateChange(false);\n this.stopPing();\n\n this.sendMessage({ type: 'close' });\n\n if (this.popOutWindow) {\n this.popOutWindow.close();\n this.popOutWindow = null;\n }\n\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: Partial): void {\n if (!this.channel) return;\n\n // Merge with current theme if not provided\n const finalConfig = {\n theme: this.currentTheme,\n ...config\n };\n\n // Update local theme cache if provided in config\n if (config.theme) {\n this.currentTheme = config.theme;\n }\n\n this.sendMessage({\n type: 'config',\n data: finalConfig\n });\n }\n\n /**\n * Update the viewer theme.\n * @param theme - Theme name (e.g. 'dark', 'light')\n */\n updateTheme(theme: string): void {\n if (this.currentTheme === theme) return;\n\n this.currentTheme = theme;\n this.sendConfig({ theme });\n Logger.debug(`[PopOut] Theme updated to: ${theme}`);\n }\n\n /**\n * Send inspector info to the pop-out viewer.\n * @param info - Inspector panel information\n */\n sendInfo(info: PopOutInfo | null): void {\n if (!this.isOpen || !this.channel) return;\n\n // Sanitize the info object to remove non-serializable data\n const sanitizedInfo = this.sanitizeInfo(info);\n\n this.sendMessage({\n type: 'info',\n data: sanitizedInfo || undefined\n });\n }\n\n /**\n * Handle node list request from popout viewer.\n * Fetches nodes from canvas and sends them to the popout.\n */\n private handleNodeListRequest(sortBy: 'title' | 'execOrder' | 'id'): void {\n if (!this.channel) return;\n\n try {\n // Access nodes from ComfyUI's app object\n const app = (window as any).app;\n const nodes = app?.graph?._nodes || [];\n\n let nodeList: { id: number; title: string; type: string; order?: number }[];\n\n if (sortBy === 'execOrder') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown',\n order: n.order ?? -1\n }))\n .filter((n: any) => n.order >= 0)\n .sort((a: any, b: any) => a.order - b.order);\n } else if (sortBy === 'id') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.id - b.id);\n } else {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.title.localeCompare(b.title));\n }\n\n this.sendMessage({\n type: 'nodes-list',\n data: { nodes: nodeList, sortBy }\n });\n\n Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);\n } catch (e) {\n Logger.error('[PopOut] Failed to get node list:', e);\n }\n }\n\n /**\n * Sanitize info object for BroadcastChannel transfer.\n * Removes functions, circular references, and non-serializable data.\n * Handles both GatheredInfo and PopOutInfo formats.\n */\n private sanitizeInfo(info: any): PopOutInfo | null {\n if (!info) return null;\n\n try {\n // Extract only the serializable parts we need\n const sanitized: PopOutInfo = {};\n\n // Handle hoveredNode\n if (info.hoveredNode) {\n sanitized.hoveredNode = {\n title: String(info.hoveredNode.title || ''),\n type: String(info.hoveredNode.type || ''),\n id: info.hoveredNode.id,\n mode: info.hoveredNode.mode,\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined,\n pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : undefined,\n // Map formatting or raw values\n pos: info.hoveredNode.position && typeof info.hoveredNode.position === 'object'\n ? { x: info.hoveredNode.position.x, y: info.hoveredNode.position.y }\n : String(info.hoveredNode.position || ''),\n size: info.hoveredNode.size && typeof info.hoveredNode.size === 'object'\n ? { w: info.hoveredNode.size.width, h: info.hoveredNode.size.height }\n : String(info.hoveredNode.size || ''),\n widgets: this.extractSafeData(info.hoveredNode.widgets),\n inputs: this.extractSafeData(info.hoveredNode.inputs),\n outputs: this.extractSafeData(info.hoveredNode.outputs),\n properties: this.sanitizeOptions(info.hoveredNode.properties) || {}\n };\n }\n\n // Handle cursor - map from GatheredInfo format (canvasX/canvasY)\n if (info.cursor) {\n sanitized.cursor = {\n canvas: {\n x: Number(info.cursor.canvasX || info.cursor.canvas?.x || 0),\n y: Number(info.cursor.canvasY || info.cursor.canvas?.y || 0)\n }\n };\n }\n\n // Handle canvas/zoom - map from GatheredInfo (zoom) to canvas.scale\n if (info.zoom !== undefined || info.canvas) {\n sanitized.canvas = {\n scale: Number(info.zoom || info.canvas?.scale || 1)\n };\n }\n\n // Handle magnifier\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass?.config) {\n sanitized.magnifier = {\n zoomFactor: Number(magnifyGlass.config.zoomFactor || 1)\n };\n }\n\n // Handle media - map from GatheredInfo mediaElement\n if (info.mediaElement || info.media) {\n const media = info.mediaElement || info.media;\n sanitized.media = {\n tagName: String(media.tagName || media.type || ''),\n naturalSize: media.naturalWidth && media.naturalHeight\n ? `${media.naturalWidth}×${media.naturalHeight}`\n : media.naturalSize\n };\n }\n\n return sanitized;\n } catch (e) {\n Logger.error('[PopOut] Failed to sanitize info:', e);\n return null;\n }\n }\n\n /**\n * Extract specific safe fields from node lists (widgets, inputs, outputs).\n * Removes functions like onFloatValueChange which cause cloning errors.\n */\n private extractSafeData(list: any[] | undefined): any[] {\n if (!list || !Array.isArray(list)) return [];\n return list.map(item => {\n if (!item || typeof item !== 'object') return {};\n\n // Only extract known safe identifiers and values\n const safeItem: any = {\n name: item.name,\n type: item.type,\n label: item.label\n };\n\n // Safely copy value if it's not a function\n if (item.value !== undefined && typeof item.value !== 'function') {\n safeItem.value = item.value;\n }\n\n // Sanitized options for widgets\n if (item.options) {\n safeItem.options = this.sanitizeOptions(item.options);\n }\n\n return safeItem;\n });\n }\n\n /**\n * Shallow copy object removing functions.\n */\n private sanitizeOptions(options: any): any {\n if (!options || typeof options !== 'object') return {};\n const clean: any = {};\n for (const key in options) {\n const val = options[key];\n if (typeof val !== 'function' && !key.startsWith('_')) {\n // Determine if value is simple enough to copy\n if (typeof val === 'object' && val !== null) {\n try {\n // Simple check if it's a plain object/array without cycles\n JSON.stringify(val);\n clean[key] = val;\n } catch (e) {\n // Skip complex objects\n }\n } else {\n clean[key] = val;\n }\n }\n }\n return clean;\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n // Define ping function\n const ping = () => {\n if (this.channel) {\n this.channel.postMessage({ type: 'ping', timestamp: Date.now() });\n }\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing(); // Stop pinging if timeout occurs (assume truly gone)\n // Actually, if we want auto-reconnect, we should NOT stop pinging on timeout?\n // But timeout implies \"It was open, but stopped responding\".\n // If user closed it, we got 'close' message.\n // If it crashed, we get timeout.\n // If we stop pinging, we won't detect if it comes back?\n // But usually we want to stop to save resources.\n // Let's stick to original behavior for timeout: stop pinging.\n // But for 'close' message: keep pinging (handled in handleMessage).\n if (this.onStateChange) this.onStateChange(false);\n }\n };\n\n // Always send an immediate ping when startPing is called, \n // to ensure responsiveness (e.g. when clicking Open button).\n ping();\n\n // If interval is already running, we don't need to start another one\n if (this.pingInterval) return;\n\n // Then ping every second\n this.pingInterval = window.setInterval(ping, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\nimport { WidgetSyncManager } from '../info-panel/widget-editors/WidgetSyncManager';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node' | 'widget-edit' | 'button-click';\n data?: string | Partial | PopOutInfo | NodeListData | number | WidgetEditData;\n timestamp?: number;\n}\n\n/**\n * Widget edit command data from pop-out viewer\n */\ninterface WidgetEditData {\n nodeId: number;\n widgetName: string;\n value: unknown;\n}\n\n/**\n * Node list data sent to popout for dropdown\n */\ninterface NodeListData {\n nodes: { id: number; title: string; type: string; order?: number }[];\n sortBy: 'title' | 'execOrder' | 'id';\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n theme?: string;\n}\n\n/**\n * Inspector info data structure for pop-out viewer\n */\ninterface PopOutInfo {\n hoveredNode?: {\n title: string;\n type: string;\n id?: number;\n mode?: string | number;\n pos?: { x: number; y: number } | string;\n size?: { w: number; h: number } | string;\n executionOrder?: number;\n category?: string;\n pythonModule?: string;\n widgets?: any[];\n inputs?: any[];\n outputs?: any[];\n properties?: Record;\n };\n cursor?: {\n canvas?: { x: number; y: number };\n };\n canvas?: {\n scale: number;\n };\n magnifier?: {\n zoomFactor: number;\n };\n media?: {\n tagName: string;\n naturalSize?: string;\n };\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => void) | null = null;\n public onNodeSelect: ((nodeId: number) => void) | null = null;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n private currentTheme: string = 'dark'; // Default theme\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Cache-busting version - increment to force refresh\n const version = 'v25';\n\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);\n\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n Logger.debug(`[PopOut] First script src: ${src}`);\n\n // src might be like: /extensions/comfyui-magnifyglass/magnify-glass/MagnifyGlass.js\n // We need to get to the web root: /extensions/comfyui-magnifyglass/\n const urlParts = src.split('/');\n // Find the extension folder (contains 'magnify' in name)\n let extensionIndex = -1;\n for (let i = 0; i < urlParts.length; i++) {\n if (urlParts[i].toLowerCase().includes('magnify')) {\n extensionIndex = i;\n break;\n }\n }\n if (extensionIndex >= 0) {\n const baseUrl = urlParts.slice(0, extensionIndex + 1).join('/');\n const viewerUrl = `${baseUrl}/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using viewer URL: ${viewerUrl}`);\n return viewerUrl;\n }\n }\n // Fallback: assume standard ComfyUI extension path\n const fallbackUrl = `/extensions/comfyui-magnifyglass/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using fallback URL: ${fallbackUrl}`);\n return fallbackUrl;\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n // Start pinging immediately to detect if viewer is already open (e.g. after reload)\n this.startPing();\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n if (this.onStateChange) this.onStateChange(true);\n // Send current theme immediately upon connection\n this.sendConfig({ theme: this.currentTheme });\n }\n break;\n case 'close':\n this.isOpen = false;\n this.popOutWindow = null;\n // Don't stop pinging, we might want to reconnect if user re-opens externally\n // or if we just want to keep checking. \n // However, original logic stopped ping. Let's keep pinging to auto-detect return.\n // this.stopPing(); \n Logger.debug('[PopOut] Viewer tab closed');\n if (this.onStateChange) this.onStateChange(false);\n break;\n case 'request-nodes':\n // Popout is requesting the node list for dropdown\n Logger.debug('[PopOut] Received request-nodes:', message.data);\n this.handleNodeListRequest(message.data as 'title' | 'execOrder' | 'id');\n break;\n case 'node-select':\n // Popout selected a node, notify main window\n if (this.onNodeSelect && typeof message.data === 'number') {\n this.onNodeSelect(message.data);\n }\n break;\n case 'zoom-node':\n // Popout requested to zoom to a node\n if (typeof message.data === 'number') {\n const app = (window as any).app;\n const node = app.graph.getNodeById(message.data);\n if (node && app.canvas) {\n app.canvas.centerOnNode(node);\n }\n }\n break;\n case 'widget-edit':\n // Popout is sending a widget value edit\n this.handleWidgetEdit(message.data as WidgetEditData);\n break;\n case 'button-click':\n // Popout is requesting a button widget callback invocation\n this.handleButtonClick(message.data as { nodeId: number; widgetName: string });\n break;\n }\n }\n\n /**\n * Handle widget edit command from pop-out viewer.\n * Syncs the edit to the actual node widget on the canvas.\n */\n private handleWidgetEdit(editData: WidgetEditData): void {\n if (!editData || typeof editData.nodeId !== 'number' || !editData.widgetName) {\n Logger.warn('[PopOut] Invalid widget edit data:', editData);\n return;\n }\n\n const result = WidgetSyncManager.syncWidgetValue(\n editData.nodeId,\n editData.widgetName,\n editData.value\n );\n\n if (result.success) {\n Logger.debug(`[PopOut] Widget edit synced: ${editData.widgetName} = ${editData.value}`);\n } else {\n Logger.warn(`[PopOut] Widget edit failed: ${result.error}`);\n }\n }\n\n /**\n * Handle button click command from pop-out viewer.\n * Invokes the widget's callback function.\n */\n private handleButtonClick(buttonData: { nodeId: number; widgetName: string }): void {\n if (!buttonData || typeof buttonData.nodeId !== 'number' || !buttonData.widgetName) {\n Logger.warn('[PopOut] Invalid button click data:', buttonData);\n return;\n }\n\n const app = (window as any).app;\n if (!app?.graph) {\n Logger.warn('[PopOut] No app.graph available for button click');\n return;\n }\n\n const node = app.graph.getNodeById(buttonData.nodeId);\n if (!node?.widgets) {\n Logger.warn(`[PopOut] Node ${buttonData.nodeId} not found or has no widgets`);\n return;\n }\n\n const widget = node.widgets.find((w: any) => w.name === buttonData.widgetName);\n if (widget && typeof widget.callback === 'function') {\n try {\n widget.callback(widget.value, app.canvas, node, [0, 0], null);\n Logger.debug(`[PopOut] Button ${buttonData.widgetName} clicked successfully`);\n } catch (error) {\n Logger.warn(`[PopOut] Button callback failed:`, error);\n }\n } else {\n Logger.warn(`[PopOut] Widget ${buttonData.widgetName} not found or has no callback`);\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n // If window exists and is open, focus it\n if (this.popOutWindow && !this.popOutWindow.closed) {\n this.popOutWindow.focus();\n return;\n }\n\n // Open new tab (or refocus existing if name matches)\n // Use a constant name to ensure singleton behavior across reloads\n this.popOutWindow = window.open(this.viewerUrl, 'MagnifyGlassPopout');\n\n if (!this.popOutWindow) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen && !this.popOutWindow) return;\n\n // update state immediately for instant UI feedback\n this.isOpen = false;\n if (this.onStateChange) this.onStateChange(false);\n this.stopPing();\n\n this.sendMessage({ type: 'close' });\n\n if (this.popOutWindow) {\n this.popOutWindow.close();\n this.popOutWindow = null;\n }\n\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: Partial): void {\n if (!this.channel) return;\n\n // Merge with current theme if not provided\n const finalConfig = {\n theme: this.currentTheme,\n ...config\n };\n\n // Update local theme cache if provided in config\n if (config.theme) {\n this.currentTheme = config.theme;\n }\n\n this.sendMessage({\n type: 'config',\n data: finalConfig\n });\n }\n\n /**\n * Update the viewer theme.\n * @param theme - Theme name (e.g. 'dark', 'light')\n */\n updateTheme(theme: string): void {\n if (this.currentTheme === theme) return;\n\n this.currentTheme = theme;\n this.sendConfig({ theme });\n Logger.debug(`[PopOut] Theme updated to: ${theme}`);\n }\n\n /**\n * Send inspector info to the pop-out viewer.\n * @param info - Inspector panel information\n */\n sendInfo(info: PopOutInfo | null): void {\n if (!this.isOpen || !this.channel) return;\n\n // Sanitize the info object to remove non-serializable data\n const sanitizedInfo = this.sanitizeInfo(info);\n\n this.sendMessage({\n type: 'info',\n data: sanitizedInfo || undefined\n });\n }\n\n /**\n * Handle node list request from popout viewer.\n * Fetches nodes from canvas and sends them to the popout.\n */\n private handleNodeListRequest(sortBy: 'title' | 'execOrder' | 'id'): void {\n if (!this.channel) return;\n\n try {\n // Access nodes from ComfyUI's app object\n const app = (window as any).app;\n const nodes = app?.graph?._nodes || [];\n\n let nodeList: { id: number; title: string; type: string; order?: number }[];\n\n if (sortBy === 'execOrder') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown',\n order: n.order ?? -1\n }))\n .filter((n: any) => n.order >= 0)\n .sort((a: any, b: any) => a.order - b.order);\n } else if (sortBy === 'id') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.id - b.id);\n } else {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.title.localeCompare(b.title));\n }\n\n this.sendMessage({\n type: 'nodes-list',\n data: { nodes: nodeList, sortBy }\n });\n\n Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);\n } catch (e) {\n Logger.error('[PopOut] Failed to get node list:', e);\n }\n }\n\n /**\n * Sanitize info object for BroadcastChannel transfer.\n * Removes functions, circular references, and non-serializable data.\n * Handles both GatheredInfo and PopOutInfo formats.\n */\n private sanitizeInfo(info: any): PopOutInfo | null {\n if (!info) return null;\n\n try {\n // Extract only the serializable parts we need\n const sanitized: PopOutInfo = {};\n\n // Handle hoveredNode\n if (info.hoveredNode) {\n sanitized.hoveredNode = {\n title: String(info.hoveredNode.title || ''),\n type: String(info.hoveredNode.type || ''),\n id: info.hoveredNode.id,\n mode: info.hoveredNode.mode,\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined,\n pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : undefined,\n // Map formatting or raw values\n pos: info.hoveredNode.position && typeof info.hoveredNode.position === 'object'\n ? { x: info.hoveredNode.position.x, y: info.hoveredNode.position.y }\n : String(info.hoveredNode.position || ''),\n size: info.hoveredNode.size && typeof info.hoveredNode.size === 'object'\n ? { w: info.hoveredNode.size.width, h: info.hoveredNode.size.height }\n : String(info.hoveredNode.size || ''),\n widgets: this.extractSafeData(info.hoveredNode.widgets),\n inputs: this.extractSafeData(info.hoveredNode.inputs),\n outputs: this.extractSafeData(info.hoveredNode.outputs),\n properties: this.sanitizeOptions(info.hoveredNode.properties) || {}\n };\n }\n\n // Handle cursor - map from GatheredInfo format (canvasX/canvasY)\n if (info.cursor) {\n sanitized.cursor = {\n canvas: {\n x: Number(info.cursor.canvasX || info.cursor.canvas?.x || 0),\n y: Number(info.cursor.canvasY || info.cursor.canvas?.y || 0)\n }\n };\n }\n\n // Handle canvas/zoom - map from GatheredInfo (zoom) to canvas.scale\n if (info.zoom !== undefined || info.canvas) {\n sanitized.canvas = {\n scale: Number(info.zoom || info.canvas?.scale || 1)\n };\n }\n\n // Handle magnifier\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass?.config) {\n sanitized.magnifier = {\n zoomFactor: Number(magnifyGlass.config.zoomFactor || 1)\n };\n }\n\n // Handle media - map from GatheredInfo mediaElement\n if (info.mediaElement || info.media) {\n const media = info.mediaElement || info.media;\n sanitized.media = {\n tagName: String(media.tagName || media.type || ''),\n naturalSize: media.naturalWidth && media.naturalHeight\n ? `${media.naturalWidth}×${media.naturalHeight}`\n : media.naturalSize\n };\n }\n\n return sanitized;\n } catch (e) {\n Logger.error('[PopOut] Failed to sanitize info:', e);\n return null;\n }\n }\n\n /**\n * Extract specific safe fields from node lists (widgets, inputs, outputs).\n * Removes functions like onFloatValueChange which cause cloning errors.\n */\n private extractSafeData(list: any[] | undefined): any[] {\n if (!list || !Array.isArray(list)) return [];\n return list.map(item => {\n if (!item || typeof item !== 'object') return {};\n\n // Only extract known safe identifiers and values\n const safeItem: any = {\n name: item.name,\n type: item.type,\n label: item.label\n };\n\n // Safely copy value if it's not a function\n if (item.value !== undefined && typeof item.value !== 'function') {\n safeItem.value = item.value;\n }\n\n // Handle widget options - preserve arrays for combo dropdowns\n if (item.options !== undefined) {\n if (Array.isArray(item.options)) {\n // For combo widgets, options is an array of values - keep it as is\n safeItem.options = item.options.filter(\n (opt: any) => typeof opt !== 'function'\n );\n } else if (typeof item.options === 'object' && item.options !== null) {\n // Check for options.values (ComfyUI nested format for combo widgets)\n if (Array.isArray(item.options.values)) {\n safeItem.options = item.options.values.filter(\n (opt: any) => typeof opt !== 'function'\n );\n } else {\n // For other widgets, options is an object with config (min, max, step, etc)\n safeItem.options = this.sanitizeOptions(item.options);\n }\n }\n }\n\n return safeItem;\n });\n }\n\n /**\n * Shallow copy object removing functions.\n */\n private sanitizeOptions(options: any): any {\n if (!options || typeof options !== 'object') return {};\n const clean: any = {};\n for (const key in options) {\n const val = options[key];\n if (typeof val !== 'function' && !key.startsWith('_')) {\n // Determine if value is simple enough to copy\n if (typeof val === 'object' && val !== null) {\n try {\n // Simple check if it's a plain object/array without cycles\n JSON.stringify(val);\n clean[key] = val;\n } catch (e) {\n // Skip complex objects\n }\n } else {\n clean[key] = val;\n }\n }\n }\n return clean;\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n // Define ping function\n const ping = () => {\n if (this.channel) {\n this.channel.postMessage({ type: 'ping', timestamp: Date.now() });\n }\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing(); // Stop pinging if timeout occurs (assume truly gone)\n // Actually, if we want auto-reconnect, we should NOT stop pinging on timeout?\n // But timeout implies \"It was open, but stopped responding\".\n // If user closed it, we got 'close' message.\n // If it crashed, we get timeout.\n // If we stop pinging, we won't detect if it comes back?\n // But usually we want to stop to save resources.\n // Let's stick to original behavior for timeout: stop pinging.\n // But for 'close' message: keep pinging (handled in handleMessage).\n if (this.onStateChange) this.onStateChange(false);\n }\n };\n\n // Always send an immediate ping when startPing is called, \n // to ensure responsiveness (e.g. when clicking Open button).\n ping();\n\n // If interval is already running, we don't need to start another one\n if (this.pingInterval) return;\n\n // Then ping every second\n this.pingInterval = window.setInterval(ping, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/popout-viewer.html b/web/popout-viewer.html index 71fe208..050e8c3 100644 --- a/web/popout-viewer.html +++ b/web/popout-viewer.html @@ -157,6 +157,54 @@ --accent-green: #a6e22e; } + /* Obsidian Theme - deep purple/charcoal */ + [data-theme="obsidian"] { + --bg-primary: rgb(30, 30, 40); + --bg-secondary: rgb(40, 40, 55); + --bg-tertiary: rgb(50, 50, 65); + --bg-elevated: rgb(45, 45, 60); + --border-subtle: rgba(80, 80, 100, 0.3); + --border-default: rgba(80, 80, 100, 0.5); + --text-primary: #d4d4d8; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + --accent-blue: #8b5cf6; + --accent-green: #a78bfa; + --accent-purple: #8b5cf6; + } + + /* Obsidian Dark Theme - even darker obsidian */ + [data-theme="obsidian-dark"] { + --bg-primary: rgb(15, 15, 20); + --bg-secondary: rgb(25, 25, 35); + --bg-tertiary: rgb(35, 35, 45); + --bg-elevated: rgb(30, 30, 40); + --border-subtle: rgba(60, 60, 80, 0.3); + --border-default: rgba(60, 60, 80, 0.5); + --text-primary: #c4c4cc; + --text-secondary: #9494a0; + --text-tertiary: #64647a; + --accent-blue: #a78bfa; + --accent-green: #c4b5fd; + --accent-purple: #a78bfa; + } + + /* Milk White Theme - soft cream/white */ + [data-theme="milk-white"] { + --bg-primary: rgb(255, 253, 250); + --bg-secondary: rgb(250, 248, 245); + --bg-tertiary: rgb(245, 243, 240); + --bg-elevated: rgb(255, 255, 255); + --border-subtle: rgba(220, 215, 210, 0.5); + --border-default: rgba(220, 215, 210, 0.8); + --text-primary: #3d3d3d; + --text-secondary: #5a5a5a; + --text-tertiary: #888888; + --accent-blue: #7c3aed; + --accent-green: #059669; + --accent-purple: #7c3aed; + } + * { margin: 0; padding: 0; @@ -803,14 +851,17 @@ border: 1px solid transparent; transition: border-color 0.15s ease; } + .widget-value-editable:hover { border-color: var(--accent-blue); } + .widget-editor-inline { display: flex; align-items: center; gap: 4px; } + .widget-editor-input { font-family: 'JetBrains Mono', monospace; font-size: 12px; @@ -822,9 +873,11 @@ width: 100%; outline: none; } + .widget-editor-input:focus { box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.25); } + .widget-editor-select { font-family: 'JetBrains Mono', monospace; font-size: 12px; @@ -1086,61 +1139,81 @@ }); } + /** + * Send button click to main window via BroadcastChannel + * This invokes the widget's callback function on the main canvas + */ + function sendButtonClick(nodeId, widgetName) { + channel.postMessage({ + type: 'button-click', + data: { + nodeId: nodeId, + widgetName: widgetName + }, + timestamp: Date.now() + }); + } + /** * Start editing a widget value inline */ function editWidgetValue(element, nodeId, widgetName, currentValue, widgetType, options) { // Already editing? if (element.querySelector('.widget-editor-inline')) return; - + const originalHtml = element.innerHTML; - const isCombo = widgetType === 'combo' && Array.isArray(options) && options.length > 0; + // Check for combo by looking at options array, not just type + const isCombo = Array.isArray(options) && options.length > 0; const isNumber = widgetType === 'number' || widgetType === 'int' || widgetType === 'float'; const isBoolean = widgetType === 'toggle' || widgetType === 'boolean'; - + let editorHtml; - + if (isCombo) { - const optionsHtml = options.map(opt => + const optionsHtml = options.map(opt => `` ).join(''); editorHtml = ``; } else if (isBoolean) { - // Toggle immediately for boolean - const newValue = currentValue !== true && currentValue !== 'true'; + // Read the CURRENT displayed value from the element text, not the stale parameter + const displayedText = element.textContent?.trim().toLowerCase(); + const currentBool = displayedText === 'true'; + const newValue = !currentBool; sendWidgetEdit(nodeId, widgetName, newValue); - element.innerHTML = `${newValue ? 'true' : 'false'}`; + // Update display - keep it simple, just update text and color + element.textContent = newValue ? 'true' : 'false'; + element.style.color = newValue ? 'var(--accent-green)' : 'var(--text-tertiary)'; return; } else { const inputType = isNumber ? 'number' : 'text'; editorHtml = ``; + onkeydown="if(event.key==='Enter'){this.blur();}if(event.key==='Escape'){cancelWidgetEdit(this,'${escapeHtml(originalHtml.replace(/'/g, "\\\\'"))}');}"/>`; } - + element.innerHTML = `
${editorHtml}
`; - + const input = element.querySelector('input, select'); if (input) { input.focus(); if (input.select) input.select(); } } - + /** * Commit widget edit and send to main window */ function commitWidgetEdit(input, nodeId, widgetName) { const value = input.type === 'number' ? parseFloat(input.value) : input.value; sendWidgetEdit(nodeId, widgetName, value); - + // Update display const container = input.closest('.widget-value-editable'); if (container) { container.innerHTML = `${escapeHtml(String(value))}`; } } - + /** * Cancel widget edit */ @@ -1248,7 +1321,7 @@ ${renderList('Inputs', info.hoveredNode.inputs)} ${renderList('Outputs', info.hoveredNode.outputs)} - ${renderList('Widgets', info.hoveredNode.widgets)} + ${renderList('Widgets', info.hoveredNode.widgets, info.hoveredNode.id)} `; @@ -1317,7 +1390,7 @@ return String(pos); } - function renderList(title, items) { + function renderList(title, items, nodeId = null) { if (!items || items.length === 0) return ''; // Icon mapping for different list types @@ -1344,16 +1417,150 @@ }; const badgeColor = badgeColors[title] || '#888'; + // Check if this is the Widgets section and we have a nodeId for editing + const isEditable = title === 'Widgets' && nodeId != null; + const listHtml = items.map((item, index) => { const name = escapeHtml(item.name || item.label || 'Unknown'); const type = item.type ? escapeHtml(item.type) : null; const rawValue = item.value !== undefined ? String(item.value) : null; const itemId = `${title.toLowerCase()}-${index}`; + // Detect toggle-style "button" widgets (have boolean values) + // These are buttons that toggle on/off state + const isButtonType = type && type.toLowerCase() === 'button'; + const hasBooleanValue = rawValue === 'true' || rawValue === 'false'; + const isToggleButton = isButtonType && hasBooleanValue; + + // True action buttons have null/undefined values or are explicitly action-oriented + const isActionButton = isButtonType && !hasBooleanValue && + (rawValue === 'null' || rawValue === 'undefined' || rawValue === null); + // Check if value contains a data URI image (chart/plot) const isImageValue = rawValue && rawValue.includes("data:image"); const value = isImageValue ? rawValue : (rawValue ? escapeHtml(rawValue) : null); + // For toggle-style button widgets, render a toggle button with on/off visual indication + if (isToggleButton && nodeId != null) { + const isOn = rawValue === 'true'; + const toggleColor = isOn ? 'var(--accent-green, #22c55e)' : 'var(--text-tertiary, #666)'; + const toggleBg = isOn ? 'var(--accent-green, #22c55e)' : 'var(--bg-tertiary, #333)'; + const toggleText = isOn ? 'ON' : 'OFF'; + + return ` +
+
+ ${name} + TOGGLE +
+ +
`; + } + + // For true action button widgets, render a clickable styled button + if (isActionButton && nodeId != null) { + return ` +
+
+ ${name} + ACTION +
+ +
`; + } + // For image values, render directly without escaping if (isImageValue) { return ` @@ -1390,6 +1597,16 @@ `; } + // Get widget options for combo boxes + const widgetOptions = item.options ? JSON.stringify(item.options).replace(/"/g, '"') : '[]'; + + // Build onclick handler for editable widgets + const editableClass = isEditable ? 'widget-value-editable' : ''; + const editableClick = isEditable && rawValue != null ? + `onclick="editWidgetValue(this, ${nodeId}, '${escapeHtml(item.name || '')}', ${JSON.stringify(rawValue).replace(/"/g, '"')}, '${type || 'text'}', ${widgetOptions})"` : ''; + const editableStyle = isEditable ? 'cursor: pointer;' : ''; + const editableTitle = isEditable ? 'title="Click to edit"' : ''; + return `
${type}` : ''} + ${isEditable ? `✏️ editable` : ''}
${value ? `
-
${value}
-
` : ''} `; }).join('');