diff --git a/.gitignore b/.gitignore index 8dde837..b1796e8 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ DEBUG_SUMMARY.md # Environment .env -.env.local \ No newline at end of file +.env.local + +# Images +pasted file.png \ No newline at end of file diff --git a/css/info-panel.css b/css/info-panel.css index e3283cc..61c08c9 100644 --- a/css/info-panel.css +++ b/css/info-panel.css @@ -46,10 +46,12 @@ border-radius: 12px; backdrop-filter: blur(20px); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); - overflow: hidden; + overflow: visible; font-size: 16px; line-height: 1.6; pointer-events: auto; + display: flex; + flex-direction: column; } .magnify-info-panel * { @@ -165,10 +167,44 @@ .magnify-info-panel.panel-minimized .panel-content { max-height: 0 !important; + height: 0 !important; overflow: hidden; + padding: 0 !important; + margin: 0 !important; transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +/* Hover-expand: Show ALL content at once - no scrollbar, full expansion */ +.magnify-info-panel.is-expanded:not(.panel-minimized) { + max-height: none !important; + overflow: visible !important; +} + +.magnify-info-panel.is-expanded:not(.panel-minimized) .panel-content { + max-height: none !important; + overflow: visible !important; +} + +/* + * STICKY MODE (PERSIST) OVERRIDES + * When sticky mode is active, we enforce the max-height and enable scrolling. + * This ensures the panel respects user-configured height and can scroll. + */ +.magnify-info-panel.persist-active:not(.panel-minimized) { + /* Use the CSS variable set by JS for max-height */ + max-height: var(--panel-max-height, 600px) !important; + overflow: hidden !important; +} + +.magnify-info-panel.persist-active:not(.panel-minimized) .panel-content { + /* Allow scrolling inside the content area */ + overflow-y: auto !important; + /* Reset max-height so it fills the parent (which is constrained) */ + max-height: none !important; + /* Ensure it takes available space */ + flex: 1 1 auto; +} + .floating-controls { position: fixed; display: none; @@ -310,8 +346,9 @@ .panel-content { padding: 0; - max-height: calc(100% - 60px); - overflow-y: auto; + flex: 1; + min-height: 0; + overflow: visible; scrollbar-width: thin; } @@ -853,16 +890,33 @@ width: 100%; cursor: pointer; appearance: none; + /* Match the blue text theme style */ + background: transparent; + border: 1px solid transparent; + color: var(--info-panel-accent-color, #74b9ff); + font-size: 14px; + padding: 4px 24px 4px 10px; + border-radius: 6px; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); background-repeat: no-repeat; - background-position: right 8px center; - background-size: 14px; - padding-right: 28px; + background-position: right 6px center; + background-size: 12px; +} + +.widget-editor-select:hover { + background-color: rgba(116, 185, 255, 0.12); + border-color: transparent; +} + +.widget-editor-select:focus { + background-color: rgba(116, 185, 255, 0.15); + border-color: var(--info-panel-accent-color, #74b9ff); + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2); } .widget-editor-select option { - background: #2a2a2a; - color: #e0e0e0; + background: #1a1a1f; + color: var(--info-panel-accent-color, #74b9ff); padding: 8px; } @@ -1049,18 +1103,31 @@ /* Theme-specific adjustments for editors */ .magnify-info-panel.theme-light .widget-editor-input, -.magnify-info-panel.theme-light .widget-editor-textarea, -.magnify-info-panel.theme-light .widget-editor-select { +.magnify-info-panel.theme-light .widget-editor-textarea { background: rgba(0, 0, 0, 0.05); border-color: rgba(0, 0, 0, 0.2); } .magnify-info-panel.theme-light .widget-editor-input:focus, -.magnify-info-panel.theme-light .widget-editor-textarea:focus, -.magnify-info-panel.theme-light .widget-editor-select:focus { +.magnify-info-panel.theme-light .widget-editor-textarea:focus { background: rgba(0, 0, 0, 0.08); } +.magnify-info-panel.theme-light .widget-editor-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #0066cc); +} + +.magnify-info-panel.theme-light .widget-editor-select:hover { + background-color: rgba(0, 102, 204, 0.08); +} + +.magnify-info-panel.theme-light .widget-editor-select option { + background: #ffffff; + color: var(--info-panel-accent-color, #0066cc); +} + .magnify-info-panel.theme-light .widget-editor-stepper { border-color: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.05); @@ -1072,14 +1139,243 @@ } .magnify-info-panel.theme-github .widget-editor-input, -.magnify-info-panel.theme-github .widget-editor-textarea, -.magnify-info-panel.theme-github .widget-editor-select { +.magnify-info-panel.theme-github .widget-editor-textarea { background: #f6f8fa; border-color: #d1d5db; color: #24292e; } +.magnify-info-panel.theme-github .widget-editor-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #0366d6); +} + +.magnify-info-panel.theme-github .widget-editor-select:hover { + background-color: rgba(3, 102, 214, 0.08); +} + .magnify-info-panel.theme-github .widget-editor-select option { background: #ffffff; - color: #24292e; + color: var(--info-panel-accent-color, #0366d6); +} + +/* ========================================================================== + Inline Controls - Compact controls that render directly in rows + ========================================================================== */ + +.inline-control-container { + display: flex; + align-items: center; + margin-left: auto; +} + +.inline-control { + display: inline-flex; + align-items: center; +} + +/* Inline Toggle - Compact version */ +.inline-toggle-switch { + position: relative; + display: inline-block; + width: 36px; + height: 20px; + cursor: pointer; +} + +.inline-toggle-input { + opacity: 0; + width: 0; + height: 0; +} + +.inline-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 20px; + transition: all 0.25s ease; + border: 1px solid rgba(255, 255, 255, 0.25); +} + +.inline-toggle-slider::before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 2px; + bottom: 2px; + background: white; + border-radius: 50%; + transition: all 0.25s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.inline-toggle-input:checked+.inline-toggle-slider { + background: var(--info-panel-accent-color, #74b9ff); + border-color: var(--info-panel-accent-color, #74b9ff); +} + +.inline-toggle-input:checked+.inline-toggle-slider::before { + transform: translateX(16px); +} + +.inline-toggle-input:focus+.inline-toggle-slider { + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25); +} + +/* Inline Dropdown - Compact version matching blue text theme */ +.inline-dropdown { + min-width: 100px; + max-width: 180px; +} + +.inline-dropdown-select { + width: 100%; + padding: 4px 24px 4px 10px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + /* Match the blue text theme style */ + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + color: var(--info-panel-accent-color, #74b9ff); + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 6px center; + background-size: 12px; + transition: all 0.2s ease; +} + +.inline-dropdown-select:hover { + background-color: rgba(116, 185, 255, 0.12); + border-color: transparent; +} + +.inline-dropdown-select:focus { + outline: none; + background-color: rgba(116, 185, 255, 0.15); + border-color: var(--info-panel-accent-color, #74b9ff); + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2); +} + +.inline-dropdown-select option { + background: #1a1a1f; + color: var(--info-panel-accent-color, #74b9ff); + padding: 4px; +} + +/* ========================================================================== + Drag Gesture Indicators + ========================================================================== */ + +.info-row.draggable { + position: relative; + cursor: ew-resize; + user-select: none; +} + +/* Value container in draggable rows should show text cursor for click-to-edit */ +.info-row.draggable .info-value { + cursor: text; + position: relative; + z-index: 1; +} + +.info-row.draggable .info-value:hover { + background: rgba(116, 185, 255, 0.18); + border-color: rgba(116, 185, 255, 0.4); +} + +.info-row.draggable::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: linear-gradient(90deg, + transparent 0%, + rgba(116, 185, 255, 0.3) 20%, + rgba(116, 185, 255, 0.3) 80%, + transparent 100%); + opacity: 0; + transition: opacity 0.2s ease; +} + +.info-row.draggable:hover::after { + opacity: 1; +} + +.info-row.dragging { + background: rgba(116, 185, 255, 0.1) !important; +} + +.info-row.dragging .info-value { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 600; +} + +.drag-indicator { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 10px; + opacity: 0; + color: rgba(255, 255, 255, 0.4); + pointer-events: none; + transition: opacity 0.2s ease; +} + +.info-row.draggable:hover .drag-indicator { + opacity: 0.6; +} + +.info-row.dragging .drag-indicator { + opacity: 1; + color: var(--info-panel-accent-color, #74b9ff); +} + +/* Theme-specific adjustments for inline controls */ +.magnify-info-panel.theme-light .inline-toggle-slider { + background: rgba(0, 0, 0, 0.15); + border-color: rgba(0, 0, 0, 0.2); +} + +.magnify-info-panel.theme-light .inline-dropdown-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #0066cc); +} + +.magnify-info-panel.theme-light .inline-dropdown-select:hover { + background-color: rgba(0, 102, 204, 0.08); +} + +.magnify-info-panel.theme-light .inline-dropdown-select option { + background: #ffffff; + color: var(--info-panel-accent-color, #0066cc); +} + +.magnify-info-panel.theme-github .inline-dropdown-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #0366d6); +} + +.magnify-info-panel.theme-github .inline-dropdown-select:hover { + background-color: rgba(3, 102, 214, 0.08); +} + +.magnify-info-panel.theme-github .inline-dropdown-select option { + background: #ffffff; + color: var(--info-panel-accent-color, #0366d6); } \ No newline at end of file diff --git a/src/info-panel/UIManager.ts b/src/info-panel/UIManager.ts index 1574a45..ff7430c 100644 --- a/src/info-panel/UIManager.ts +++ b/src/info-panel/UIManager.ts @@ -20,7 +20,14 @@ import { type ParameterItem } from './NodeDataExtractor'; import { NodeSelector, type NodeListEntry, type NodeExecOrderEntry } from './NodeSelector'; -import { WidgetEditorFactory, WidgetSyncManager, type WidgetEditorInstance } from './widget-editors'; +import { + WidgetEditorFactory, + WidgetSyncManager, + InlineControlFactory, + DragValueController, + type WidgetEditorInstance, + type InlineControlInstance +} from './widget-editors'; interface InfoPanelElements { panel: HTMLDivElement | null; @@ -41,6 +48,10 @@ export class UIManager { onNodeSelected: ((nodeId: number) => void) | null = null; // Track active widget editors for cleanup private activeEditors: Map = new Map(); + // Track active inline controls for cleanup + private activeInlineControls: Map = new Map(); + // Track active drag controllers for cleanup + private activeDragControllers: Map = new Map(); constructor(stateManager: StateManager) { this.stateManager = stateManager; @@ -718,9 +729,17 @@ export class UIManager { /** * Display information in the panel. - * @param info + * @param info */ displayInfo(info: any): void { + // Skip re-rendering if user is actively editing a value + // This prevents the editor from being destroyed mid-edit + if (this.activeEditors.size > 0) { + // Only update the header subtitle, don't re-render content + this.updateHeaderSubtitle(info); + return; + } + const sections = this.buildSections(info); this.renderSections(sections); this.updateSectionStates(); @@ -920,22 +939,12 @@ export class UIManager { // Only show dropdown arrow for actual dropdowns, not actions like zoom const dropdownIcon = (item.clickable && item.clickable !== 'zoom') ? '▼' : ''; - // Show copy button for string values that are long enough to be worth copying - const rawValue = String(item.value || ''); - const showCopyBtn = !item.clickable && rawValue.length > 3 && typeof item.value === 'string'; - const copyBtnHtml = showCopyBtn ? ` - ` : ''; return `
${escapeHtml(item.label)} ${value}${dropdownIcon} + - ${copyBtnHtml}
`; }).join('')} @@ -952,7 +961,7 @@ export class UIManager { } /** - * Cleanup active widget editors. + * Cleanup active widget editors, inline controls, and drag controllers. */ private cleanupEditors(): void { this.activeEditors.forEach(editor => { @@ -963,10 +972,29 @@ export class UIManager { } }); this.activeEditors.clear(); + + this.activeInlineControls.forEach(control => { + try { + control.destroy(); + } catch (e) { + // Ignore cleanup errors + } + }); + this.activeInlineControls.clear(); + + this.activeDragControllers.forEach(controller => { + try { + controller.destroy(); + } catch (e) { + // Ignore cleanup errors + } + }); + this.activeDragControllers.clear(); } /** * Attach click handlers to editable widget rows. + * Also creates inline controls for toggle/combo widgets and drag controllers. */ private attachEditableRowHandlers(): void { if (!this.elements.content) return; @@ -976,14 +1004,78 @@ export class UIManager { const rowEl = row as HTMLElement; const valueEl = rowEl.querySelector('.info-value.original') as HTMLElement; const editorContainer = rowEl.querySelector('.widget-editor-container') as HTMLElement; + const inlineContainer = rowEl.querySelector('.inline-control-container') as HTMLElement; - if (!valueEl || !editorContainer) return; + if (!valueEl) return; - // Click on value to enter edit mode - valueEl.addEventListener('click', (e) => { - e.stopPropagation(); - this.enterEditMode(rowEl, valueEl, editorContainer); - }); + const nodeId = parseInt(rowEl.dataset.nodeId || '0', 10); + const widgetName = rowEl.dataset.widgetName || ''; + const widgetType = rowEl.dataset.widgetType || 'text'; + const rawValue = rowEl.dataset.rawValue; + + // Parse constraints + let constraints: any = {}; + try { + const constraintsStr = rowEl.dataset.constraints; + if (constraintsStr) { + constraints = JSON.parse(constraintsStr); + } + } catch (e) { + // Ignore parse errors + } + + if (isNaN(nodeId) || !widgetName) return; + + const controlKey = `${nodeId}:${widgetName}`; + + // Check if this widget type should use inline controls + if (InlineControlFactory.shouldUseInlineControl(widgetType) && inlineContainer) { + // Create inline control + const control = InlineControlFactory.createControl({ + nodeId, + widgetName, + widgetType, + currentValue: widgetType.toLowerCase() === 'boolean' || widgetType.toLowerCase() === 'toggle' + ? rawValue === 'true' + : rawValue, + constraints, + onChange: (value) => { + rowEl.dataset.rawValue = String(value); + } + }); + + if (control) { + this.activeInlineControls.set(controlKey, control); + inlineContainer.appendChild(control.element); + inlineContainer.style.display = 'flex'; + // Hide the text value display + valueEl.style.display = 'none'; + } + } else if (editorContainer) { + // Fall back to click-to-edit for text/number types + valueEl.addEventListener('click', (e) => { + e.stopPropagation(); + this.enterEditMode(rowEl, valueEl, editorContainer); + }); + } + + // Attach drag controller if widget type supports it and we don't have an inline control + if (DragValueController.isTypeSupported(widgetType) && + !InlineControlFactory.shouldUseInlineControl(widgetType)) { + const dragController = new DragValueController(rowEl, { + nodeId, + widgetName, + widgetType, + currentValue: rawValue, + constraints, + onChange: (value) => { + rowEl.dataset.rawValue = String(value); + // Update displayed value + valueEl.textContent = formatWidgetValue(value); + } + }); + this.activeDragControllers.set(controlKey, dragController); + } }); } @@ -1122,27 +1214,6 @@ export class UIManager { (row as HTMLElement).style.background = ''; }); }); - - // Add click handlers for copy buttons - const copyButtons = this.elements.content.querySelectorAll('.copy-btn'); - copyButtons.forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const value = (btn as HTMLElement).dataset.copyValue || ''; - navigator.clipboard.writeText(value).then(() => { - // Show success feedback - btn.classList.add('copied'); - const originalHtml = btn.innerHTML; - btn.innerHTML = ``; - setTimeout(() => { - btn.innerHTML = originalHtml; - btn.classList.remove('copied'); - }, 1500); - }).catch(err => { - console.error('Failed to copy:', err); - }); - }); - }); } /** diff --git a/src/info-panel/widget-editors/DragValueController.ts b/src/info-panel/widget-editors/DragValueController.ts new file mode 100644 index 0000000..33b20fa --- /dev/null +++ b/src/info-panel/widget-editors/DragValueController.ts @@ -0,0 +1,288 @@ +/** + * ComfyUI MagnifyGlass - Drag Value Controller + * + * Handles horizontal drag gestures on inspector rows for quick value changes. + * Allows users to drag left/right to adjust numbers or cycle through options. + */ + +import { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager'; +import { Logger } from '../../shared/logger'; + +/** + * Configuration for drag behavior + */ +export interface DragConfig { + nodeId: number; + widgetName: string; + widgetType: string; + currentValue: unknown; + constraints?: WidgetConstraints; + onChange?: (value: unknown) => void; +} + +/** + * Default sensitivity settings + */ +const DRAG_SENSITIVITY = { + // Pixels per unit change for integers + intPixelsPerUnit: 8, + // Pixels per 0.1 change for floats + floatPixelsPerTenth: 10, + // Minimum pixels to trigger option cycle for combos + comboThreshold: 40, +}; + +/** + * Drag Value Controller + * Manages drag interactions for inline value editing + */ +export class DragValueController { + private element: HTMLElement; + private config: DragConfig; + private isDragging: boolean = false; + private startX: number = 0; + private startValue: number = 0; + private startOptionIndex: number = 0; + private accumulatedDelta: number = 0; + + // Bound event handlers for cleanup + private boundPointerMove: (e: PointerEvent) => void; + private boundPointerUp: (e: PointerEvent) => void; + + constructor(element: HTMLElement, config: DragConfig) { + this.element = element; + this.config = config; + + this.boundPointerMove = this.onPointerMove.bind(this); + this.boundPointerUp = this.onPointerUp.bind(this); + + this.init(); + } + + /** + * Initialize drag handling + */ + private init(): void { + this.element.classList.add('draggable'); + + this.element.addEventListener('pointerdown', this.onPointerDown.bind(this)); + + // Add visual indicator + // this.addDragIndicator(); + } + + /** + * Add visual drag indicator to the row + */ + private addDragIndicator(): void { + // Check if already has indicator + if (this.element.querySelector('.drag-indicator')) return; + + const indicator = document.createElement('span'); + indicator.className = 'drag-indicator'; + indicator.innerHTML = '⟷'; // Left-right arrow + indicator.setAttribute('aria-hidden', 'true'); + this.element.appendChild(indicator); + } + + /** + * Handle pointer down - start drag tracking + */ + private onPointerDown(e: PointerEvent): void { + // Only handle left mouse button or touch + if (e.button !== 0) return; + + // Don't start drag if clicking on interactive elements or value/editor areas + // Only the label area should initiate drag - all controls should be clickable + const target = e.target as HTMLElement; + if (target.tagName === 'INPUT' || + target.tagName === 'SELECT' || + target.tagName === 'BUTTON' || + target.closest('.inline-control') || + target.closest('.inline-control-container') || + target.closest('.info-value') || + target.closest('.widget-editor') || + target.closest('.widget-editor-container') || + target.closest('.widget-editor-stepper') || + target.closest('.widget-editor-number') || + target.closest('.widget-editor-input')) { + return; + } + + this.isDragging = true; + this.startX = e.clientX; + this.accumulatedDelta = 0; + + // Capture current value + const type = this.config.widgetType.toLowerCase(); + if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') { + this.startValue = Number(this.config.currentValue) || 0; + } else if (type === 'combo') { + const options = this.config.constraints?.options ?? []; + const currentStr = String(this.config.currentValue); + this.startOptionIndex = options.findIndex(opt => String(opt) === currentStr); + if (this.startOptionIndex === -1) this.startOptionIndex = 0; + } + + // Add dragging class + this.element.classList.add('dragging'); + + // Capture pointer for reliable tracking + this.element.setPointerCapture(e.pointerId); + + // Add document-level listeners + document.addEventListener('pointermove', this.boundPointerMove); + document.addEventListener('pointerup', this.boundPointerUp); + + e.preventDefault(); + Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`); + } + + /** + * Handle pointer move - update value based on drag distance + */ + private onPointerMove(e: PointerEvent): void { + if (!this.isDragging) return; + + const deltaX = e.clientX - this.startX; + const type = this.config.widgetType.toLowerCase(); + + if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') { + this.handleNumberDrag(deltaX); + } else if (type === 'combo') { + this.handleComboDrag(deltaX); + } + } + + /** + * Handle number value drag + */ + private handleNumberDrag(deltaX: number): void { + const constraints = this.config.constraints; + const isInt = this.config.widgetType.toLowerCase() === 'int'; + + // Calculate sensitivity based on type and constraints + let sensitivity: number; + if (isInt) { + sensitivity = DRAG_SENSITIVITY.intPixelsPerUnit; + } else { + // For floats, adjust based on step + const step = constraints?.step ?? 0.1; + sensitivity = DRAG_SENSITIVITY.floatPixelsPerTenth / (step / 0.1); + } + + // Calculate new value + const units = deltaX / sensitivity; + const step = constraints?.step ?? (isInt ? 1 : 0.1); + let newValue = this.startValue + (units * step); + + // Apply constraints + if (constraints?.min !== undefined) { + newValue = Math.max(constraints.min, newValue); + } + if (constraints?.max !== undefined) { + newValue = Math.min(constraints.max, newValue); + } + + // Round to step precision + if (step) { + newValue = Math.round(newValue / step) * step; + } + + // Round for display + if (isInt) { + newValue = Math.round(newValue); + } else { + const precision = constraints?.precision ?? 2; + newValue = Number(newValue.toFixed(precision)); + } + + // Sync the value + WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue); + this.config.currentValue = newValue; + this.config.onChange?.(newValue); + } + + /** + * Handle combo value drag - cycle through options + */ + private handleComboDrag(deltaX: number): void { + const options = this.config.constraints?.options ?? []; + if (options.length === 0) return; + + this.accumulatedDelta = deltaX; + + // Calculate how many options to move + const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold); + + if (optionDelta > 0) { + const direction = deltaX > 0 ? 1 : -1; + let newIndex = this.startOptionIndex + (optionDelta * direction); + + // Wrap around + newIndex = ((newIndex % options.length) + options.length) % options.length; + + const newValue = options[newIndex]; + WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue); + this.config.currentValue = newValue; + this.config.onChange?.(newValue); + } + } + + /** + * Handle pointer up - end drag + */ + private onPointerUp(e: PointerEvent): void { + if (!this.isDragging) return; + + this.isDragging = false; + this.element.classList.remove('dragging'); + + // Release pointer capture + try { + this.element.releasePointerCapture(e.pointerId); + } catch { + // Ignore if already released + } + + // Remove document-level listeners + document.removeEventListener('pointermove', this.boundPointerMove); + document.removeEventListener('pointerup', this.boundPointerUp); + + Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`); + } + + /** + * Update the current value (call when value changes externally) + */ + updateValue(value: unknown): void { + this.config.currentValue = value; + } + + /** + * Cleanup and remove drag handling + */ + destroy(): void { + this.element.classList.remove('draggable', 'dragging'); + + // Remove indicator + const indicator = this.element.querySelector('.drag-indicator'); + if (indicator) { + indicator.remove(); + } + + // Remove document listeners if still attached + document.removeEventListener('pointermove', this.boundPointerMove); + document.removeEventListener('pointerup', this.boundPointerUp); + } + + /** + * Check if a widget type supports drag editing + */ + static isTypeSupported(widgetType: string): boolean { + const type = widgetType.toLowerCase(); + return [ + 'number', 'int', 'float', 'slider', 'combo' + ].includes(type); + } +} diff --git a/src/info-panel/widget-editors/InlineControlFactory.ts b/src/info-panel/widget-editors/InlineControlFactory.ts new file mode 100644 index 0000000..a7658d2 --- /dev/null +++ b/src/info-panel/widget-editors/InlineControlFactory.ts @@ -0,0 +1,149 @@ +/** + * ComfyUI MagnifyGlass - Inline Control Factory + * + * Creates compact inline controls that render directly in inspector rows + * for immediate value editing without entering edit mode. + */ + +import { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager'; +import { Logger } from '../../shared/logger'; + +/** + * Configuration for creating an inline control + */ +export interface InlineControlConfig { + nodeId: number; + widgetName: string; + widgetType: string; + currentValue: unknown; + constraints?: WidgetConstraints; + onChange?: (value: unknown) => void; +} + +/** + * Interface for inline control instances + */ +export interface InlineControlInstance { + element: HTMLElement; + getValue(): unknown; + setValue(value: unknown): void; + destroy(): void; +} + +/** + * Inline Control Factory + * Creates compact inline controls for immediate value editing + */ +export class InlineControlFactory { + /** + * Check if a widget type should use inline controls + */ + static shouldUseInlineControl(widgetType: string): boolean { + const type = widgetType.toLowerCase(); + return type === 'toggle' || type === 'boolean' || type === 'combo'; + } + + /** + * Create an inline control for the given configuration + */ + static createControl(config: InlineControlConfig): InlineControlInstance | null { + const type = config.widgetType.toLowerCase(); + + switch (type) { + case 'toggle': + case 'boolean': + return this.createInlineToggle(config); + case 'combo': + return this.createInlineDropdown(config); + default: + return null; + } + } + + /** + * Create compact inline toggle for boolean values + */ + private static createInlineToggle(config: InlineControlConfig): InlineControlInstance { + const container = document.createElement('div'); + container.className = 'inline-control inline-toggle'; + + const label = document.createElement('label'); + label.className = 'inline-toggle-switch'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'inline-toggle-input'; + // Handle boolean coercion properly + const boolValue = config.currentValue === true || config.currentValue === 'true'; + checkbox.checked = boolValue; + + const slider = document.createElement('span'); + slider.className = 'inline-toggle-slider'; + + // Stop propagation to prevent row click handlers + container.addEventListener('click', (e) => e.stopPropagation()); + + // Sync on change + checkbox.addEventListener('change', () => { + WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked); + config.onChange?.(checkbox.checked); + Logger.debug(`[InlineControl] Toggle ${config.widgetName}: ${checkbox.checked}`); + }); + + label.appendChild(checkbox); + label.appendChild(slider); + container.appendChild(label); + + return { + element: container, + getValue: () => checkbox.checked, + setValue: (v) => { checkbox.checked = Boolean(v); }, + destroy: () => { container.remove(); } + }; + } + + /** + * Create compact inline dropdown for combo values + */ + private static createInlineDropdown(config: InlineControlConfig): InlineControlInstance { + const container = document.createElement('div'); + container.className = 'inline-control inline-dropdown'; + + const select = document.createElement('select'); + select.className = 'inline-dropdown-select'; + + // Populate options + const options = config.constraints?.options ?? []; + const currentValueStr = String(config.currentValue); + + for (const opt of options) { + const option = document.createElement('option'); + const optStr = String(opt); + option.value = optStr; + option.textContent = optStr; + if (optStr === currentValueStr) { + option.selected = true; + } + select.appendChild(option); + } + + // Stop propagation to prevent row click handlers + container.addEventListener('click', (e) => e.stopPropagation()); + + // Sync on change + select.addEventListener('change', () => { + WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value); + config.onChange?.(select.value); + Logger.debug(`[InlineControl] Dropdown ${config.widgetName}: ${select.value}`); + }); + + container.appendChild(select); + + return { + element: container, + getValue: () => select.value, + setValue: (v) => { select.value = String(v); }, + destroy: () => { container.remove(); } + }; + } +} diff --git a/src/info-panel/widget-editors/WidgetEditorFactory.ts b/src/info-panel/widget-editors/WidgetEditorFactory.ts index cafdd4a..6ade3c1 100644 --- a/src/info-panel/widget-editors/WidgetEditorFactory.ts +++ b/src/info-panel/widget-editors/WidgetEditorFactory.ts @@ -150,9 +150,10 @@ export class WidgetEditorFactory { syncValue(); }); - // Handle keyboard + // Handle keyboard - Enter to confirm, Escape to cancel input.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + if (e.key === 'Escape' || e.key === 'Enter') { + e.preventDefault(); input.blur(); } }); @@ -282,7 +283,8 @@ export class WidgetEditorFactory { select.addEventListener('blur', () => config.onBlur?.()); select.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + if (e.key === 'Escape' || e.key === 'Enter') { + e.preventDefault(); select.blur(); } }); diff --git a/src/info-panel/widget-editors/index.ts b/src/info-panel/widget-editors/index.ts index 01491d5..b61507e 100644 --- a/src/info-panel/widget-editors/index.ts +++ b/src/info-panel/widget-editors/index.ts @@ -6,3 +6,5 @@ export { WidgetSyncManager, type WidgetConstraints, type SyncResult } from './WidgetSyncManager'; export { WidgetEditorFactory, type WidgetEditorConfig, type WidgetEditorInstance } from './WidgetEditorFactory'; +export { InlineControlFactory, type InlineControlConfig, type InlineControlInstance } from './InlineControlFactory'; +export { DragValueController, type DragConfig } from './DragValueController'; diff --git a/web/info-panel.css b/web/info-panel.css index f7c091a..45fb048 100644 --- a/web/info-panel.css +++ b/web/info-panel.css @@ -851,4 +851,531 @@ height: 14px; stroke-width: 2.5; /* Make icon slightly bolder */ -} \ No newline at end of file +} +/* ========================================================================== + Widget Editor Styles - Inspector Inline Editing + ========================================================================== */ + +/* Base widget editor container */ +.widget-editor { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + box-sizing: border-box; +} + +/* Shared input styles */ +.widget-editor-input, +.widget-editor-textarea, +.widget-editor-select { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--info-panel-accent-color, #74b9ff); + border-radius: 4px; + color: inherit; + padding: 6px 8px; + outline: none; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.widget-editor-input:focus, +.widget-editor-textarea:focus, +.widget-editor-select:focus { + border-color: var(--info-panel-accent-color, #74b9ff); + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25); + background: rgba(0, 0, 0, 0.4); +} + +.widget-editor-input:hover, +.widget-editor-textarea:hover, +.widget-editor-select:hover { + border-color: rgba(116, 185, 255, 0.7); +} + +/* Number editor specific */ +.widget-editor-number { + display: flex; + align-items: center; + gap: 2px; +} + +.widget-editor-number .widget-editor-input { + flex: 1; + min-width: 60px; + max-width: 100px; + text-align: center; + appearance: textfield; + -moz-appearance: textfield; +} + +.widget-editor-number .widget-editor-input::-webkit-outer-spin-button, +.widget-editor-number .widget-editor-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.widget-editor-stepper { + width: 24px; + height: 28px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.1); + color: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + transition: all 0.15s ease; + border-radius: 4px; +} + +.widget-editor-stepper:hover { + background: rgba(255, 255, 255, 0.2); + border-color: var(--info-panel-accent-color, #74b9ff); +} + +.widget-editor-stepper:active { + background: rgba(255, 255, 255, 0.3); + transform: scale(0.95); +} + +.widget-editor-stepper.decrement { + border-radius: 4px 0 0 4px; +} + +.widget-editor-stepper.increment { + border-radius: 0 4px 4px 0; +} + +/* Text editor specific */ +.widget-editor-text { + flex-direction: column; + align-items: stretch; + gap: 4px; +} + +.widget-editor-text .widget-editor-input { + width: 100%; +} + +.widget-editor-textarea { + width: 100%; + min-height: 60px; + max-height: 200px; + resize: vertical; + line-height: 1.4; +} + +.widget-editor-counter { + font-size: 11px; + opacity: 0.6; + text-align: right; + padding-right: 4px; +} + +/* Combo/dropdown editor - blue text theme style */ +.widget-editor-combo { + width: 100%; +} + +.widget-editor-select { + width: 100%; + cursor: pointer; + appearance: none; + background: transparent; + border: 1px solid transparent; + color: var(--info-panel-accent-color, #74b9ff); + font-size: 14px; + padding: 4px 24px 4px 10px; + border-radius: 6px; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 6px center; + background-size: 12px; +} + +.widget-editor-select:hover { + background-color: rgba(116, 185, 255, 0.12); + border-color: transparent; +} + +.widget-editor-select:focus { + background-color: rgba(116, 185, 255, 0.15); + border-color: var(--info-panel-accent-color, #74b9ff); + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2); +} + +.widget-editor-select option { + background: #1a1a1f; + color: var(--info-panel-accent-color, #74b9ff); + padding: 8px; +} + +/* Boolean toggle editor */ +.widget-editor-boolean { + justify-content: flex-start; +} + +.widget-editor-toggle { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + cursor: pointer; +} + +.widget-editor-checkbox { + opacity: 0; + width: 0; + height: 0; +} + +.widget-editor-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 24px; + transition: all 0.3s ease; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.widget-editor-toggle-slider::before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 2px; + bottom: 2px; + background: white; + border-radius: 50%; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.widget-editor-checkbox:checked+.widget-editor-toggle-slider { + background: var(--info-panel-accent-color, #74b9ff); + border-color: var(--info-panel-accent-color, #74b9ff); +} + +.widget-editor-checkbox:checked+.widget-editor-toggle-slider::before { + transform: translateX(20px); +} + +.widget-editor-checkbox:focus+.widget-editor-toggle-slider { + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25); +} + +/* Slider editor */ +.widget-editor-slider { + gap: 8px; +} + +.widget-editor-range { + flex: 1; + height: 6px; + -webkit-appearance: none; + appearance: none; + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + outline: none; + cursor: pointer; +} + +.widget-editor-range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--info-panel-accent-color, #74b9ff); + cursor: pointer; + border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.15s ease; +} + +.widget-editor-range::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 0 4px rgba(116, 185, 255, 0.25); +} + +.widget-editor-range::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--info-panel-accent-color, #74b9ff); + cursor: pointer; + border: 2px solid white; +} + +.widget-editor-slider-value { + min-width: 50px; + text-align: center; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(0, 0, 0, 0.2); + padding: 4px 8px; + border-radius: 4px; +} + +/* Editable row styling */ +.info-row.editable { + cursor: pointer; +} + +.info-row.editable:hover { + background: rgba(116, 185, 255, 0.08); +} + +.info-row.editable .info-value { + cursor: text; + border: 1px solid transparent; +} + +.info-row.editable .info-value:hover { + border-color: rgba(116, 185, 255, 0.3); +} + +.info-row.editing { + background: rgba(116, 185, 255, 0.1); + padding: 8px; + margin: 4px -8px; + border-radius: 6px; +} + +.info-row.editing .info-label { + margin-bottom: 4px; +} + +/* Hide original value when editing */ +.info-row.editing .info-value.original { + display: none; +} + +/* ========================================================================== + Inline Controls - Compact controls that render directly in rows + ========================================================================== */ + +.inline-control-container { + display: flex; + align-items: center; + margin-left: auto; +} + +.inline-control { + display: inline-flex; + align-items: center; +} + +/* Inline Toggle - Compact version */ +.inline-toggle-switch { + position: relative; + display: inline-block; + width: 36px; + height: 20px; + cursor: pointer; +} + +.inline-toggle-input { + opacity: 0; + width: 0; + height: 0; +} + +.inline-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 20px; + transition: all 0.25s ease; + border: 1px solid rgba(255, 255, 255, 0.25); +} + +.inline-toggle-slider::before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 2px; + bottom: 2px; + background: white; + border-radius: 50%; + transition: all 0.25s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.inline-toggle-input:checked+.inline-toggle-slider { + background: var(--info-panel-accent-color, #74b9ff); + border-color: var(--info-panel-accent-color, #74b9ff); +} + +.inline-toggle-input:checked+.inline-toggle-slider::before { + transform: translateX(16px); +} + +.inline-toggle-input:focus+.inline-toggle-slider { + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25); +} + +/* Inline Dropdown - blue text theme style */ +.inline-dropdown { + min-width: 100px; + max-width: 180px; +} + +.inline-dropdown-select { + width: 100%; + padding: 4px 24px 4px 10px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + color: var(--info-panel-accent-color, #74b9ff); + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 6px center; + background-size: 12px; + transition: all 0.2s ease; +} + +.inline-dropdown-select:hover { + background-color: rgba(116, 185, 255, 0.12); + border-color: transparent; +} + +.inline-dropdown-select:focus { + outline: none; + background-color: rgba(116, 185, 255, 0.15); + border-color: var(--info-panel-accent-color, #74b9ff); + box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2); +} + +.inline-dropdown-select option { + background: #1a1a1f; + color: var(--info-panel-accent-color, #74b9ff); + padding: 4px; +} + +/* ========================================================================== + Drag Gesture Indicators + ========================================================================== */ + +.info-row.draggable { + position: relative; + cursor: ew-resize; + user-select: none; +} + +/* Value container in draggable rows should show text cursor for click-to-edit */ +.info-row.draggable .info-value { + cursor: text; + position: relative; + z-index: 1; +} + +.info-row.draggable .info-value:hover { + background: rgba(116, 185, 255, 0.18); + border-color: rgba(116, 185, 255, 0.4); +} + +.info-row.draggable::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: linear-gradient(90deg, + transparent 0%, + rgba(116, 185, 255, 0.3) 20%, + rgba(116, 185, 255, 0.3) 80%, + transparent 100%); + opacity: 0; + transition: opacity 0.2s ease; +} + +.info-row.draggable:hover::after { + opacity: 1; +} + +.info-row.dragging { + background: rgba(116, 185, 255, 0.1) !important; +} + +.info-row.dragging .info-value { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 600; +} + +.drag-indicator { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 10px; + opacity: 0; + color: rgba(255, 255, 255, 0.4); + pointer-events: none; + transition: opacity 0.2s ease; +} + +.info-row.draggable:hover .drag-indicator { + opacity: 0.6; +} + +.info-row.dragging .drag-indicator { + opacity: 1; + color: var(--info-panel-accent-color, #74b9ff); +} + +/* Theme-specific adjustments for inline controls */ +.magnify-info-panel.theme-light .inline-toggle-slider { + background: rgba(0, 0, 0, 0.15); + border-color: rgba(0, 0, 0, 0.2); +} + +.magnify-info-panel.theme-light .inline-dropdown-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #0066cc); +} + +.magnify-info-panel.theme-light .inline-dropdown-select:hover { + background-color: rgba(0, 102, 204, 0.08); +} + +.magnify-info-panel.theme-light .inline-dropdown-select option { + background: #ffffff; + color: var(--info-panel-accent-color, #0066cc); +} + +.magnify-info-panel.theme-github .inline-dropdown-select { + background: transparent; + border-color: transparent; + color: var(--info-panel-accent-color, #0366d6); +} + +.magnify-info-panel.theme-github .inline-dropdown-select:hover { + background-color: rgba(3, 102, 214, 0.08); +} + +.magnify-info-panel.theme-github .inline-dropdown-select option { + background: #ffffff; + color: var(--info-panel-accent-color, #0366d6); +} diff --git a/web/info-panel/UIManager.js b/web/info-panel/UIManager.js index 805b4cf..2b0b2cf 100644 --- a/web/info-panel/UIManager.js +++ b/web/info-panel/UIManager.js @@ -10,6 +10,8 @@ import { getCheckpointInfo, getImageInfo, getTextBoxContent, getImportantNodePar import { NodeSelector } from "./NodeSelector.js"; import { WidgetSyncManager } from "./widget-editors/WidgetSyncManager.js"; import { WidgetEditorFactory } from "./widget-editors/WidgetEditorFactory.js"; +import { InlineControlFactory } from "./widget-editors/InlineControlFactory.js"; +import { DragValueController } from "./widget-editors/DragValueController.js"; class UIManager { constructor(stateManager) { __publicField(this, "stateManager"); @@ -19,6 +21,10 @@ class UIManager { __publicField(this, "onNodeSelected", null); // Track active widget editors for cleanup __publicField(this, "activeEditors", /* @__PURE__ */ new Map()); + // Track active inline controls for cleanup + __publicField(this, "activeInlineControls", /* @__PURE__ */ new Map()); + // Track active drag controllers for cleanup + __publicField(this, "activeDragControllers", /* @__PURE__ */ new Map()); this.stateManager = stateManager; this.elements = { panel: null, @@ -516,9 +522,13 @@ class UIManager { } /** * Display information in the panel. - * @param info + * @param info */ displayInfo(info) { + if (this.activeEditors.size > 0) { + this.updateHeaderSubtitle(info); + return; + } const sections = this.buildSections(info); this.renderSections(sections); this.updateSectionStates(); @@ -662,21 +672,12 @@ class UIManager { const rawValueStr = isEditable ? typeof item.rawValue === "boolean" ? item.rawValue ? "true" : "false" : escapeHtml(String(item.rawValue ?? "")) : ""; const editableAttrs = isEditable ? `data-editable="true" data-widget-name="${escapeHtml(item.widgetName)}" data-widget-type="${escapeHtml(item.widgetType || "text")}" data-raw-value="${rawValueStr}" data-constraints="${constraintsJson}"` : ""; const dropdownIcon = item.clickable && item.clickable !== "zoom" ? '▼' : ""; - const rawValue = String(item.value || ""); - const showCopyBtn = !item.clickable && rawValue.length > 3 && typeof item.value === "string"; - const copyBtnHtml = showCopyBtn ? ` - ` : ""; return `
${escapeHtml(item.label)} ${value}${dropdownIcon} + - ${copyBtnHtml}
`; }).join("")} @@ -688,7 +689,7 @@ class UIManager { } } /** - * Cleanup active widget editors. + * Cleanup active widget editors, inline controls, and drag controllers. */ cleanupEditors() { this.activeEditors.forEach((editor) => { @@ -698,9 +699,24 @@ class UIManager { } }); this.activeEditors.clear(); + this.activeInlineControls.forEach((control) => { + try { + control.destroy(); + } catch (e) { + } + }); + this.activeInlineControls.clear(); + this.activeDragControllers.forEach((controller) => { + try { + controller.destroy(); + } catch (e) { + } + }); + this.activeDragControllers.clear(); } /** * Attach click handlers to editable widget rows. + * Also creates inline controls for toggle/combo widgets and drag controllers. */ attachEditableRowHandlers() { if (!this.elements.content) return; @@ -709,11 +725,59 @@ class UIManager { const rowEl = row; const valueEl = rowEl.querySelector(".info-value.original"); const editorContainer = rowEl.querySelector(".widget-editor-container"); - if (!valueEl || !editorContainer) return; - valueEl.addEventListener("click", (e) => { - e.stopPropagation(); - this.enterEditMode(rowEl, valueEl, editorContainer); - }); + const inlineContainer = rowEl.querySelector(".inline-control-container"); + if (!valueEl) return; + const nodeId = parseInt(rowEl.dataset.nodeId || "0", 10); + const widgetName = rowEl.dataset.widgetName || ""; + const widgetType = rowEl.dataset.widgetType || "text"; + const rawValue = rowEl.dataset.rawValue; + let constraints = {}; + try { + const constraintsStr = rowEl.dataset.constraints; + if (constraintsStr) { + constraints = JSON.parse(constraintsStr); + } + } catch (e) { + } + if (isNaN(nodeId) || !widgetName) return; + const controlKey = `${nodeId}:${widgetName}`; + if (InlineControlFactory.shouldUseInlineControl(widgetType) && inlineContainer) { + const control = InlineControlFactory.createControl({ + nodeId, + widgetName, + widgetType, + currentValue: widgetType.toLowerCase() === "boolean" || widgetType.toLowerCase() === "toggle" ? rawValue === "true" : rawValue, + constraints, + onChange: (value) => { + rowEl.dataset.rawValue = String(value); + } + }); + if (control) { + this.activeInlineControls.set(controlKey, control); + inlineContainer.appendChild(control.element); + inlineContainer.style.display = "flex"; + valueEl.style.display = "none"; + } + } else if (editorContainer) { + valueEl.addEventListener("click", (e) => { + e.stopPropagation(); + this.enterEditMode(rowEl, valueEl, editorContainer); + }); + } + if (DragValueController.isTypeSupported(widgetType) && !InlineControlFactory.shouldUseInlineControl(widgetType)) { + const dragController = new DragValueController(rowEl, { + nodeId, + widgetName, + widgetType, + currentValue: rawValue, + constraints, + onChange: (value) => { + rowEl.dataset.rawValue = String(value); + valueEl.textContent = formatWidgetValue(value); + } + }); + this.activeDragControllers.set(controlKey, dragController); + } }); } /** @@ -817,24 +881,6 @@ class UIManager { row.style.background = ""; }); }); - const copyButtons = this.elements.content.querySelectorAll(".copy-btn"); - copyButtons.forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - const value = btn.dataset.copyValue || ""; - navigator.clipboard.writeText(value).then(() => { - btn.classList.add("copied"); - const originalHtml = btn.innerHTML; - btn.innerHTML = ``; - setTimeout(() => { - btn.innerHTML = originalHtml; - btn.classList.remove("copied"); - }, 1500); - }).catch((err) => { - console.error("Failed to copy:", err); - }); - }); - }); } /** * Update section expansion states. diff --git a/web/info-panel/UIManager.js.map b/web/info-panel/UIManager.js.map index 074cf62..99e9365 100644 --- a/web/info-panel/UIManager.js.map +++ b/web/info-panel/UIManager.js.map @@ -1 +1 @@ -{"version":3,"file":"UIManager.js","sources":["../../src/info-panel/UIManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel UI Manager (TypeScript)\n * \n * Complete UI Manager extracted from magnify_info_panel.js\n * Handles all DOM manipulation and UI creation for the info panel.\n */\n\nimport { StateManager } from './StateManager';\nimport { Icons } from '../shared/icons';\nimport { Logger } from '../shared/logger';\nimport { INFO_PANEL_ID } from '../shared/constants';\nimport { escapeHtml } from '../shared/utils';\nimport { formatValue, getValueClass, getValueAttributes, formatWidgetValue } from './ValueFormatter';\nimport {\n getCheckpointInfo,\n getImageInfo,\n getTextBoxContent,\n getImportantNodeParameters,\n type ImageInfoResult,\n type ParameterItem\n} from './NodeDataExtractor';\nimport { NodeSelector, type NodeListEntry, type NodeExecOrderEntry } from './NodeSelector';\nimport { WidgetEditorFactory, WidgetSyncManager, type WidgetEditorInstance } from './widget-editors';\n\ninterface InfoPanelElements {\n panel: HTMLDivElement | null;\n header: HTMLDivElement | null;\n content: HTMLDivElement | null;\n controls: HTMLDivElement | null;\n}\n\n/**\n * UI Manager class.\n * Handles all DOM manipulation and UI creation.\n */\nexport class UIManager {\n stateManager: StateManager;\n elements: InfoPanelElements;\n nodeSelector: NodeSelector;\n currentDropdown: HTMLDivElement | null = null;\n onNodeSelected: ((nodeId: number) => void) | null = null;\n // Track active widget editors for cleanup\n private activeEditors: Map = new Map();\n\n constructor(stateManager: StateManager) {\n this.stateManager = stateManager;\n this.elements = {\n panel: null,\n header: null,\n content: null,\n controls: null\n };\n this.nodeSelector = new NodeSelector();\n\n this.createPanel();\n this.injectStyles();\n }\n\n /**\n * Create the main panel and its components.\n */\n createPanel(): void {\n // Main panel container\n this.elements.panel = document.createElement(\"div\");\n this.elements.panel.id = INFO_PANEL_ID;\n this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`;\n\n // Header\n this.elements.header = document.createElement(\"div\");\n this.elements.header.className = \"panel-header\";\n this.elements.header.innerHTML = `\n
\n
${Icons.magnifyGlass}
\n
Inspector
\n
Real-time analysis
\n
\n
\n \n
\n `;\n\n // Content container\n this.elements.content = document.createElement(\"div\");\n this.elements.content.className = \"panel-content\";\n\n this.elements.panel.appendChild(this.elements.header);\n this.elements.panel.appendChild(this.elements.content);\n\n this.applyStyles();\n // Apply initial minimized state if set\n if (this.stateManager.state.isPanelMinimized) {\n this.elements.panel.classList.add('panel-minimized');\n this.updateMinimizedState(); // Correctly set aria-expanded state\n }\n document.body.appendChild(this.elements.panel);\n\n // Hover-expand: Add/remove 'is-expanded' class, reposition, and resize font if needed\n let originalTop: number | null = null;\n let originalFontSize: number | null = null;\n const MIN_FONT_SIZE = 10; // Minimum font size in pixels\n const MARGIN = 20; // Minimum margin from viewport edges\n\n this.elements.panel.addEventListener('mouseenter', () => {\n // Sticky Mode (Persist): disable this behavior to prevent bouncing\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n return;\n }\n\n if (!this.stateManager.state.isPanelMinimized && this.elements.panel) {\n // Save original position\n originalTop = this.elements.panel.offsetTop;\n // Save original font size just in case, though we won't change it\n originalFontSize = parseFloat(getComputedStyle(this.elements.panel).fontSize);\n\n // Add expanded class\n this.elements.panel.classList.add('is-expanded');\n\n // Simple repositioning logic only (no font scaling)\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (!this.elements.panel) return;\n\n const viewportHeight = window.innerHeight;\n const rect = this.elements.panel.getBoundingClientRect();\n\n // Position the panel to fit (move up if needed)\n if (rect.bottom > viewportHeight - MARGIN) {\n const newTop = Math.max(MARGIN, originalTop! - (rect.bottom - viewportHeight + MARGIN));\n this.elements.panel.style.top = `${newTop}px`;\n }\n });\n });\n }\n });\n\n this.elements.panel.addEventListener('mouseleave', () => {\n // Sticky Mode (Persist): disable this behavior to prevent bouncing\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n return;\n }\n\n if (this.elements.panel) {\n this.elements.panel.classList.remove('is-expanded');\n\n // Restore original position\n if (originalTop !== null) {\n this.elements.panel.style.top = `${originalTop}px`;\n originalTop = null;\n }\n\n // Restore original font size (if it was somehow changed)\n if (originalFontSize !== null) {\n this.elements.panel.style.fontSize = `${originalFontSize}px`;\n originalFontSize = null;\n }\n\n // Clear any inline styles that might have been set\n this.elements.panel.style.transform = '';\n this.elements.panel.style.transformOrigin = '';\n }\n });\n\n // Add click event delegation for panel elements\n this.elements.panel.addEventListener('click', (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n\n // Handle minimize button\n const minimizeBtn = target.closest('[data-action=\"minimize\"]');\n if (minimizeBtn) {\n this.stateManager.state.isPanelMinimized = !this.stateManager.state.isPanelMinimized;\n this.updateMinimizedState();\n return;\n }\n\n // Handle section header clicks for expand/collapse\n const sectionHeader = target.closest('.section-header') as HTMLElement;\n if (sectionHeader) {\n const sectionId = sectionHeader.getAttribute('data-section');\n // Skip node section (always expanded)\n if (sectionId && sectionId !== 'node') {\n const isExpanded = this.stateManager.state.expandedSections.has(sectionId);\n if (isExpanded) {\n this.stateManager.state.expandedSections.delete(sectionId);\n } else {\n this.stateManager.state.expandedSections.add(sectionId);\n }\n\n // Update visual state\n sectionHeader.classList.toggle('expanded', !isExpanded);\n const sectionContent = sectionHeader.nextElementSibling as HTMLElement;\n if (sectionContent && sectionContent.classList.contains('section-content')) {\n sectionContent.classList.toggle('expanded', !isExpanded);\n }\n }\n }\n });\n\n // Create floating controls after panel is in DOM\n if (!this.elements.controls) {\n this.createFloatingControls();\n // Update control states immediately after creation\n this.updateControlStates();\n }\n }\n\n /**\n * Create floating control buttons.\n */\n createFloatingControls(): void {\n this.elements.controls = document.createElement(\"div\");\n this.elements.controls.className = `floating-controls vertical-layout theme-${this.stateManager.state.currentTheme}`; // Default to vertical + theme\n\n /**\n * BUTTON MAPPING DOCUMENTATION:\n * =============================\n * Button 1 (Unlock Icon): data-action=\"pin\" → controls isPanelPinned\n * - Unpinned (default): Panel follows magnify glass\n * - Pinned: Panel stays at fixed position (unlocked from following glass)\n * \n * Button 2 (Pin Icon): data-action=\"lock\" → controls isPanelLocked\n * - Unlocked (default): Panel can be dragged\n * - Locked: Panel cannot be dragged (pinned in place)\n * \n * NOTE: The data-action names are counterintuitive but kept for backwards compatibility.\n * The ICONS correctly represent the functionality:\n * - Unlock icon = unlock from following glass\n * - Pin icon = pin position (prevent drag)\n */\n this.elements.controls.innerHTML = `\n \n \n \n \n \n \n \n \n \n \n `;\n\n // Insert before the panel in the document body, not as a child\n document.body.appendChild(this.elements.controls);\n\n // Hide and position off-screen initially - will be shown when magnify glass activates and positions them\n this.elements.controls.style.display = 'none';\n this.elements.controls.style.visibility = 'hidden';\n this.elements.controls.style.left = '-9999px';\n this.elements.controls.style.top = '-9999px';\n\n // Add click event delegation for control buttons\n this.elements.controls.addEventListener('click', (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n const button = target.closest('button[data-action]') as HTMLButtonElement;\n if (!button) return;\n\n const action = button.getAttribute('data-action');\n Logger.debug(`Control button clicked: ${action}`);\n\n switch (action) {\n case 'pin':\n this.stateManager.togglePinning();\n this.updatePinnedState();\n this.updateControlStates();\n break;\n case 'lock':\n this.stateManager.state.isPanelLocked = !this.stateManager.state.isPanelLocked;\n this.updateControlStates();\n if (this.elements.panel) {\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n }\n break;\n case 'persist':\n this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"] = !this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n this.updateControlStates();\n this.applyStyles(); // Update persist-active class\n // Sync sidebar setting if needed (optional, sidebar updates on reopen usually)\n break;\n case 'toggle-panel':\n if (this.stateManager.state.isPanelVisible) {\n // User requested to hide the panel\n this.hide();\n\n // User Request: Disable sticky info when hiding via button\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"] = false;\n this.applyStyles(); // Update visual styles if necessary\n }\n } else {\n this.show();\n }\n this.updateControlStates();\n break;\n case 'toggle-glass':\n this.stateManager.state.isGlassPreviewVisible = !this.stateManager.state.isGlassPreviewVisible;\n\n // Link Glass Visibility to Panel Pinning\n if (!this.stateManager.state.isGlassPreviewVisible) {\n // Glass Hidden\n console.log(`[MagnifyGlass Debug] Glass Hidden. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`);\n // Only auto-pin if not already pinned by the user\n if (!this.stateManager.state.isPanelPinned) {\n if (this.elements.panel) {\n const rect = this.elements.panel.getBoundingClientRect();\n this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top };\n }\n this.stateManager.state.isPanelPinned = true;\n this.stateManager.state.isAutoPinned = true; // Track that this was auto-pinned\n console.log(`[MagnifyGlass Debug] Auto-Pinned panel.`);\n }\n // Do NOT reset isPanelLocked here - preserve user's lock state\n } else {\n // Glass Shown\n console.log(`[MagnifyGlass Debug] Glass Shown. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`);\n // Only unpin if it was auto-pinned (not user-pinned)\n if (this.stateManager.state.isAutoPinned) {\n this.stateManager.state.isPanelPinned = false;\n this.stateManager.state.isAutoPinned = false;\n console.log(`[MagnifyGlass Debug] Auto-Unpinned panel.`);\n }\n // If user manually pinned, keep the panel pinned\n }\n\n this.updateControlStates();\n this.updatePinnedState(); // Update visual class\n\n // Toggle BOTH the visual visibility AND the rendering\n // This fully disables the offscreen renderer when preview is hidden\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.setGlassPreviewActive) {\n magnifyGlass.setGlassPreviewActive(this.stateManager.state.isGlassPreviewVisible);\n } else if (magnifyGlass && magnifyGlass.ui?.setPreviewVisibility) {\n // Fallback for backwards compatibility\n magnifyGlass.ui.setPreviewVisibility(this.stateManager.state.isGlassPreviewVisible);\n }\n break;\n case 'popout':\n // Toggle the pop-out tab\n const glass = (window as any).comfyUIMagnifyGlass;\n if (glass && glass.popOutManager) {\n glass.popOutManager.toggle();\n }\n break;\n case 'drag-glass':\n // Toggle glass drag mode\n const mglass = (window as any).comfyUIMagnifyGlass;\n if (mglass && mglass.state) {\n mglass.state.isDragModeEnabled = !mglass.state.isDragModeEnabled;\n this.updateControlStates();\n\n // Update UI to indicate drag mode\n if (mglass.ui && mglass.ui.setDragMode) {\n mglass.ui.setDragMode(mglass.state.isDragModeEnabled);\n }\n }\n break;\n case 'reset-glass':\n // Reset glass offsets\n const rglass = (window as any).comfyUIMagnifyGlass;\n if (rglass) {\n rglass.resetOffsets();\n Logger.debug(\"Reset glass position via hover controls\");\n\n // Disable drag mode if active\n if (rglass.state && rglass.state.isDragModeEnabled) {\n rglass.state.isDragModeEnabled = false;\n if (rglass.ui && rglass.ui.setDragMode) {\n rglass.ui.setDragMode(false);\n }\n this.updateControlStates();\n }\n }\n break;\n case 'toggle-cursor':\n const cglass = (window as any).comfyUIMagnifyGlass;\n if (cglass && cglass.config) {\n cglass.config.showCursorPreview = !cglass.config.showCursorPreview;\n cglass.updateMagnifiedView();\n this.updateControlStates();\n }\n break;\n case 'toggle-hold':\n this.stateManager.toggleHold();\n this.updateControlStates();\n break;\n }\n });\n\n // Set initial layout based on settings\n const controlsPosition = String(this.stateManager.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] || \"top-right\");\n this.updateControlsLayout(controlsPosition);\n\n // Initial state update\n this.updateControlStates();\n }\n\n /**\n * Update control button states.\n */\n updateControlStates(): void {\n if (!this.elements.controls) return;\n\n const pinBtn = this.elements.controls.querySelector('[data-action=\"pin\"]') as HTMLButtonElement;\n const lockBtn = this.elements.controls.querySelector('[data-action=\"lock\"]') as HTMLButtonElement;\n const visibilityBtn = this.elements.controls.querySelector('[data-action=\"toggle-panel\"]') as HTMLButtonElement;\n const glassBtn = this.elements.controls.querySelector('[data-action=\"toggle-glass\"]') as HTMLButtonElement;\n const isPanelVisible = this.stateManager.state.isPanelVisible;\n const isGlassVisible = this.stateManager.state.isGlassPreviewVisible;\n\n if (pinBtn) {\n pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned);\n pinBtn.setAttribute('aria-pressed', String(this.stateManager.state.isPanelPinned));\n pinBtn.title = this.stateManager.state.isPanelPinned ? \"Lock Panel\" : \"Unlock Panel\";\n pinBtn.innerHTML = this.stateManager.state.isPanelPinned ? Icons.lock : Icons.unlock;\n // Hide unlock button when panel is hidden\n pinBtn.style.display = isPanelVisible ? 'flex' : 'none';\n\n // Disable lock button when glass is hidden (no point unlocking to follow invisible glass)\n if (!isGlassVisible) {\n pinBtn.disabled = true;\n pinBtn.style.opacity = '0.5';\n pinBtn.title = \"Cannot toggle lock when glass preview is hidden\";\n } else {\n pinBtn.disabled = false;\n pinBtn.style.opacity = '';\n pinBtn.title = this.stateManager.state.isPanelPinned ? \"Lock Panel Position (Follow Glass)\" : \"Unlock Panel Position (Drag Inspector)\";\n }\n }\n\n if (lockBtn) {\n // Only show pin button when panel is visible AND unlocked from glass\n const showLockBtn = isPanelVisible && this.stateManager.state.isPanelPinned;\n lockBtn.style.display = showLockBtn ? 'flex' : 'none';\n lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked);\n lockBtn.setAttribute('aria-pressed', String(this.stateManager.state.isPanelLocked));\n lockBtn.title = this.stateManager.state.isPanelLocked ? \"Unpin Panel Position\" : \"Pin Panel Position\";\n lockBtn.disabled = !this.stateManager.state.isPanelPinned;\n }\n\n const persistBtn = this.elements.controls.querySelector('[data-action=\"persist\"]') as HTMLButtonElement;\n if (persistBtn) {\n const isPersistConfigured = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n persistBtn.classList.toggle('active', isPersistConfigured);\n persistBtn.setAttribute('aria-pressed', String(isPersistConfigured));\n persistBtn.title = isPersistConfigured ? \"Disable Sticky Info (S)\" : \"Enable Sticky Info (S)\";\n persistBtn.style.display = isPanelVisible ? 'flex' : 'none';\n }\n\n const holdBtn = this.elements.controls.querySelector('[data-action=\"toggle-hold\"]') as HTMLButtonElement;\n if (holdBtn) {\n const isPersistConfigured = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n const isHeld = this.stateManager.state.isInfoHeld;\n\n // Only show Hold button when Sticky Info (Persist) is enabled AND panel is visible\n holdBtn.style.display = (isPanelVisible && isPersistConfigured) ? 'flex' : 'none';\n\n holdBtn.classList.toggle('active', isHeld);\n holdBtn.setAttribute('aria-pressed', String(isHeld));\n // Toggle icon: Pause (to hold) vs Play (to resume)\n holdBtn.innerHTML = isHeld ? Icons.play : Icons.pause;\n holdBtn.title = isHeld ? \"Resume Info Update (P)\" : \"Pause Info Update (P)\";\n }\n\n if (visibilityBtn) {\n // Active means \"Panel is Visible\"\n visibilityBtn.classList.toggle('active', isPanelVisible);\n visibilityBtn.setAttribute('aria-pressed', String(isPanelVisible));\n visibilityBtn.title = isPanelVisible ? \"Hide Panel\" : \"Show Panel\";\n\n // Disable hide button if glass is hidden (prevent hiding everything)\n if (!isGlassVisible) {\n visibilityBtn.disabled = true;\n visibilityBtn.style.opacity = '0.5';\n visibilityBtn.title = \"Cannot hide panel when glass preview is hidden\";\n } else {\n visibilityBtn.disabled = false;\n visibilityBtn.style.opacity = '';\n }\n }\n\n if (glassBtn) {\n glassBtn.classList.toggle('active', isGlassVisible);\n glassBtn.setAttribute('aria-pressed', String(isGlassVisible));\n glassBtn.title = isGlassVisible ? \"Hide Glass Preview\" : \"Show Glass Preview\";\n\n // Only show glass toggle button if the inspector panel is visible\n glassBtn.style.display = isPanelVisible ? 'flex' : 'none';\n }\n\n const cursorBtn = this.elements.controls.querySelector('[data-action=\"toggle-cursor\"]') as HTMLButtonElement;\n if (cursorBtn) {\n const cglass = (window as any).comfyUIMagnifyGlass;\n const showCursor = cglass?.config?.showCursorPreview || false;\n cursorBtn.classList.toggle('active', showCursor);\n cursorBtn.setAttribute('aria-pressed', String(showCursor));\n cursorBtn.title = showCursor ? \"Hide Cursor Preview\" : \"Show Cursor Preview\";\n\n // Logic: Visible when glass is visible AND:\n // 1. Panel is NOT pinned (attached to glass) - original logic\n // 2. Sticky Info (Persist) is NOT enabled - user request\n const isPersistConfigured = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n\n cursorBtn.style.display = (isGlassVisible && !this.stateManager.state.isPanelPinned && !isPersistConfigured) ? 'flex' : 'none';\n }\n\n const dragGlassBtn = this.elements.controls.querySelector('[data-action=\"drag-glass\"]') as HTMLButtonElement;\n if (dragGlassBtn) {\n const mglass = (window as any).comfyUIMagnifyGlass;\n const isDragMode = mglass?.state?.isDragModeEnabled || false;\n dragGlassBtn.classList.toggle('active', isDragMode);\n dragGlassBtn.setAttribute('aria-pressed', String(isDragMode));\n dragGlassBtn.title = isDragMode ? \"Cancel Move Mode (H)\" : \"Move Glass Position (H)\";\n // Only show drag button when glass is visible (even if panel is hidden)\n dragGlassBtn.style.display = isGlassVisible ? 'flex' : 'none';\n }\n\n const resetGlassBtn = this.elements.controls.querySelector('[data-action=\"reset-glass\"]') as HTMLButtonElement;\n if (resetGlassBtn) {\n // Only show reset button when glass is visible (moves with drag/popout buttons)\n resetGlassBtn.style.display = isGlassVisible ? 'flex' : 'none';\n }\n\n const popoutBtn = this.elements.controls.querySelector('[data-action=\"popout\"]') as HTMLButtonElement;\n if (popoutBtn) {\n const glass = (window as any).comfyUIMagnifyGlass;\n const isOpen = glass?.popOutManager?.isPopOutOpen() || false;\n // Logger.debug(`[UIManager] Popout button update - Glass: ${!!glass}, IsOpen: ${isOpen}`);\n popoutBtn.classList.toggle('active', isOpen);\n popoutBtn.setAttribute('aria-pressed', String(isOpen));\n popoutBtn.title = isOpen ? \"Close Pop-out Viewer\" : \"Open Pop-out Viewer\";\n }\n }\n\n /**\n * Update controls layout based on position setting.\n * @param position \n */\n updateControlsLayout(position: string): void {\n if (!this.elements.controls) return;\n\n // Remove existing layout classes\n this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout');\n\n // Add appropriate layout class\n if (['top', 'bottom'].includes(position)) {\n this.elements.controls.classList.add('horizontal-layout');\n } else {\n this.elements.controls.classList.add('vertical-layout');\n }\n }\n\n /**\n * Load a Google Font dynamically.\n * @param fontName - Name of the font to load\n */\n loadGoogleFont(fontName: string): void {\n // Skip if already loaded\n const linkId = `google-font-${fontName.replace(/\\s+/g, '-').toLowerCase()}`;\n if (document.getElementById(linkId)) return;\n\n // Create link element for Google Fonts\n const link = document.createElement('link');\n link.id = linkId;\n link.rel = 'stylesheet';\n link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@400;500;600;700&display=swap`;\n document.head.appendChild(link);\n\n Logger.debug(`[UIManager] Loaded Google Font: ${fontName}`);\n }\n\n /**\n * Apply current styles to elements.\n */\n applyStyles(): void {\n if (!this.elements.panel) return;\n\n const settings = this.stateManager.state.settings;\n\n // Apply dimensions\n this.elements.panel.style.width = `${settings[\"🔍MagnifyGlass.InfoPanelWidth\"]}px`;\n // Set max-height via CSS variable so CSS :hover rules can override\n const maxHeight = settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"];\n this.elements.panel.style.setProperty('--panel-max-height', `${maxHeight}px`);\n this.elements.panel.style.height = 'auto';\n\n // 2. Apply static styles (only needed once or if overwritten, but safe to set)\n this.elements.panel.style.position = 'absolute';\n this.elements.panel.style.zIndex = '99999';\n this.elements.panel.style.transform = 'translateY(-10px)';\n this.elements.panel.style.pointerEvents = 'auto';\n this.elements.panel.style.userSelect = 'none';\n\n // Apply Font Family\n const fontFamily = settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"] as string || \"System Default\";\n if (fontFamily === \"System Default\" || fontFamily === \"system-ui\") {\n this.elements.panel.style.fontFamily = \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n } else if (fontFamily === \"monospace\") {\n this.elements.panel.style.fontFamily = \"'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, monospace\";\n } else {\n // Load Google Font dynamically\n this.loadGoogleFont(fontFamily);\n this.elements.panel.style.fontFamily = `'${fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;\n }\n\n // Apply Font Size\n const fontSize = settings[\"🔍MagnifyGlass.InfoPanelFontSize\"] || 14;\n this.elements.panel.style.fontSize = `${fontSize}px`;\n\n // 3. Apply transition\n this.elements.panel.style.transition = 'none';\n\n // 4. Apply Custom Colors (CSS Variables)\n const textColor = settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] as string | undefined;\n if (textColor && typeof textColor === 'string') {\n const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`;\n this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor);\n }\n\n const accentColor = settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] as string | undefined;\n if (accentColor && typeof accentColor === 'string') {\n const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`;\n this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor);\n }\n\n // 5. Apply Opacity\n // Logic: If visible, use setting. If hidden, use 0.\n // We do NOT touch 'display' here to avoid hiding it if it's currently shown.\n // 'display' is managed by show() / hide() methods exclusively.\n if (this.stateManager.state.isPanelVisible) {\n const opacityPercent = Number(settings[\"🔍MagnifyGlass.InfoPanelOpacity\"]) || 100;\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n } else {\n this.elements.panel.style.opacity = '0';\n }\n\n // 6. Apply Persist Mode Class\n const isPersist = !!settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n if (isPersist) {\n this.elements.panel.classList.add('persist-active');\n } else {\n this.elements.panel.classList.remove('persist-active');\n }\n }\n\n /**\n * Show the panel.\n */\n show(): void {\n if (!this.elements.panel) return;\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n this.elements.panel.style.display = \"block\";\n // Controls are shown by positionFloatingControls() after proper positioning\n // Apply user's opacity setting when showing (convert percentage to decimal)\n const opacityPercent = Number(this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"]) || 100;\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n // Trigger reflow\n this.elements.panel.offsetHeight;\n this.elements.panel.classList.add('visible');\n this.stateManager.state.isPanelVisible = true;\n this.updateControlStates();\n }\n }\n\n /**\n * Hide the panel.\n */\n hide(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.remove('visible');\n this.elements.panel.classList.remove('visible');\n if (this.elements.panel) {\n this.elements.panel.style.display = \"none\";\n }\n this.stateManager.state.isPanelVisible = false;\n this.updateControlStates();\n }\n\n /**\n * Update minimized state.\n */\n updateMinimizedState(): void {\n if (!this.elements.panel || !this.elements.header) return;\n\n this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized);\n const minimizeBtn = this.elements.header.querySelector('.minimize-btn');\n if (minimizeBtn) {\n minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−';\n (minimizeBtn as HTMLElement).title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel';\n minimizeBtn.setAttribute('aria-expanded', String(!this.stateManager.state.isPanelMinimized));\n }\n }\n\n /**\n * Update pinned state.\n */\n updatePinnedState(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned);\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n this.updateControlStates();\n }\n\n /**\n * Update theme class.\n * @param newTheme \n */\n updateTheme(newTheme: string): void {\n if (this.elements.panel) {\n this.elements.panel.className = this.elements.panel.className.replace(/theme-\\w+/, `theme-${newTheme.toLowerCase()}`);\n }\n if (this.elements.controls) {\n this.elements.controls.className = this.elements.controls.className.replace(/theme-\\w+/, `theme-${newTheme.toLowerCase()}`);\n }\n }\n\n /**\n * Display information in the panel.\n * @param info \n */\n displayInfo(info: any): void {\n const sections = this.buildSections(info);\n this.renderSections(sections);\n this.updateSectionStates();\n this.updateHeaderSubtitle(info);\n }\n\n /**\n * Build section data from info object.\n * @param info \n * @returns \n */\n buildSections(info: any): any[] {\n const sections: any[] = [];\n const settings = this.stateManager.state.settings;\n\n // Inspector section\n if (settings[\"🔍MagnifyGlass.ShowInspectorTab\"] && info.cursor && info.cursor.canvas) {\n const inspectorContent = [\n { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` },\n { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` },\n { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` }\n ];\n\n sections.push({\n id: 'inspector',\n icon: Icons.info,\n title: 'Inspector',\n content: inspectorContent\n });\n }\n\n // Media section\n if (info.media) {\n const mediaContent = [\n { label: 'Type', value: info.media.tagName },\n { label: 'Source', value: info.media.src }\n ];\n\n if (info.media.naturalSize) {\n mediaContent.push({ label: 'Natural', value: info.media.naturalSize });\n }\n\n sections.push({\n id: 'media',\n icon: Icons.camera,\n title: 'Media',\n badge: info.media.tagName,\n content: mediaContent\n });\n }\n\n // Node Details section\n if (info.hoveredNode) {\n const nodeContent: any[] = []; // Initial Node details\n nodeContent.push({\n label: 'Title',\n value: `${info.hoveredNode.title || 'Untitled'} (#${info.hoveredNode.id})`,\n clickable: 'title'\n });\n if (info.hoveredNode.executionOrder !== undefined) {\n nodeContent.push({ label: 'Exec Order', value: info.hoveredNode.executionOrder, clickable: 'execOrder' });\n }\n\n // Add Zoom to Node button\n nodeContent.push({\n label: 'Location',\n value: `${Icons.focus} Focus Node`,\n clickable: 'zoom',\n nodeId: info.hoveredNode.id,\n isHtml: true\n });\n\n // Add category if available\n if (info.hoveredNode.category) {\n nodeContent.push({ label: 'Category', value: info.hoveredNode.category });\n }\n\n // Add python module path if available\n if (info.hoveredNode.pythonModule) {\n const path = info.hoveredNode.pythonModule.replace(/\\./g, '/') + '.py';\n nodeContent.push({ label: 'Path', value: path });\n }\n\n // Check if this is a complex node that shows all widgets\n const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : '';\n const isSaveNode = nodeType.includes('save') &&\n !nodeType.includes('checkpoint') &&\n !nodeType.includes('model') &&\n !nodeType.includes('preview');\n\n const showAllWidgets = info.hoveredNode.type && (\n nodeType.includes('ksampler') ||\n nodeType.includes('sampler') ||\n nodeType.includes('k_samplers') ||\n nodeType.includes('checkpoint') ||\n nodeType.includes('model') ||\n nodeType.includes('lora') ||\n nodeType.includes('controlnet') ||\n nodeType.includes('advanced') ||\n nodeType.includes('detailer') ||\n nodeType.includes('inpaint') ||\n nodeType.includes('upscale') ||\n nodeType.includes('clip') ||\n nodeType.includes('text') ||\n nodeType.includes('encode')\n ) && !isSaveNode;\n\n if (!showAllWidgets) {\n // Add specific extractions for simple nodes\n const checkpoint = getCheckpointInfo(info.hoveredNode);\n if (checkpoint) {\n nodeContent.push({ label: 'Model', value: checkpoint });\n }\n\n const imgInfo = getImageInfo(info.hoveredNode);\n if (imgInfo) {\n if (typeof imgInfo === 'string') {\n nodeContent.push({ label: 'Image', value: imgInfo });\n } else if (typeof imgInfo === 'object' && imgInfo !== null) {\n const imgResult = imgInfo as ImageInfoResult;\n nodeContent.push({ label: 'Image Size', value: `${imgResult.width}×${imgResult.height}` });\n if (imgResult.src) {\n nodeContent.push({ label: 'Image Source', value: imgResult.src });\n }\n }\n }\n\n const textContent = getTextBoxContent(info.hoveredNode);\n if (textContent) {\n nodeContent.push({ label: 'Text', value: textContent });\n }\n }\n\n const importantParameters = getImportantNodeParameters(info.hoveredNode);\n nodeContent.push(...importantParameters);\n\n sections.push({\n id: 'node',\n icon: Icons.box,\n title: 'Node',\n badge: info.hoveredNode.type,\n content: nodeContent\n });\n }\n\n return sections;\n }\n\n /**\n * Render sections to the panel.\n * @param sections \n */\n renderSections(sections: any[]): void {\n if (!this.elements.content) return;\n\n // Cleanup existing editors before re-rendering\n this.cleanupEditors();\n\n if (sections.length === 0) {\n this.elements.content.innerHTML = `\n
\n
${Icons.mapPin}
\n
Empty canvas area
\n
\n `;\n return;\n }\n\n // Check if Sticky Info is enabled (editing only works when sticky)\n const isStickyEnabled = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n\n this.elements.content.innerHTML = sections.map(section => `\n
\n
\n ${section.icon}\n ${escapeHtml(section.title)}\n ${section.badge ? `${escapeHtml(section.badge)}` : ''}\n ${section.id !== 'node' ? `${Icons.chevronRight}` : ''}\n
\n
\n
\n ${section.content.map((item: any) => {\n const value = item.isHtml ? item.value : formatValue(item.value, item.label);\n const valueClass = getValueClass(item.value);\n const valueAttributes = getValueAttributes(item.value);\n const clickableAttr = item.clickable ? `data-clickable=\"${item.clickable}\"` : '';\n const nodeIdAttr = item.nodeId !== undefined ? `data-node-id=\"${item.nodeId}\"` : '';\n const clickableClass = item.clickable ? 'clickable-row' : '';\n\n // Editable widget detection\n const isEditable = item.isEditable && isStickyEnabled && item.widgetName && item.nodeId !== undefined;\n const editableClass = isEditable ? 'editable' : '';\n // Serialize constraints and rawValue properly (handle boolean specially to avoid \"false\" -> true coercion)\n const constraintsJson = isEditable && item.constraints ? escapeHtml(JSON.stringify(item.constraints)) : '';\n const rawValueStr = isEditable ? (typeof item.rawValue === 'boolean' ? (item.rawValue ? 'true' : 'false') : escapeHtml(String(item.rawValue ?? ''))) : '';\n const editableAttrs = isEditable ? `data-editable=\"true\" data-widget-name=\"${escapeHtml(item.widgetName)}\" data-widget-type=\"${escapeHtml(item.widgetType || 'text')}\" data-raw-value=\"${rawValueStr}\" data-constraints=\"${constraintsJson}\"` : '';\n\n // Only show dropdown arrow for actual dropdowns, not actions like zoom\n const dropdownIcon = (item.clickable && item.clickable !== 'zoom') ? '▼' : '';\n // Show copy button for string values that are long enough to be worth copying\n const rawValue = String(item.value || '');\n const showCopyBtn = !item.clickable && rawValue.length > 3 && typeof item.value === 'string';\n const copyBtnHtml = showCopyBtn ? `\n ` : '';\n return `\n
\n ${escapeHtml(item.label)}\n ${value}${dropdownIcon}\n
\n ${copyBtnHtml}\n
`;\n }).join('')}\n
\n
\n
`).join('');\n\n // Add click handlers for clickable rows\n this.attachDropdownClickHandlers();\n\n // Add click handlers for editable rows (only when sticky is enabled)\n if (isStickyEnabled) {\n this.attachEditableRowHandlers();\n }\n }\n\n /**\n * Cleanup active widget editors.\n */\n private cleanupEditors(): void {\n this.activeEditors.forEach(editor => {\n try {\n editor.destroy();\n } catch (e) {\n // Ignore cleanup errors\n }\n });\n this.activeEditors.clear();\n }\n\n /**\n * Attach click handlers to editable widget rows.\n */\n private attachEditableRowHandlers(): void {\n if (!this.elements.content) return;\n\n const editableRows = this.elements.content.querySelectorAll('[data-editable=\"true\"]');\n editableRows.forEach(row => {\n const rowEl = row as HTMLElement;\n const valueEl = rowEl.querySelector('.info-value.original') as HTMLElement;\n const editorContainer = rowEl.querySelector('.widget-editor-container') as HTMLElement;\n\n if (!valueEl || !editorContainer) return;\n\n // Click on value to enter edit mode\n valueEl.addEventListener('click', (e) => {\n e.stopPropagation();\n this.enterEditMode(rowEl, valueEl, editorContainer);\n });\n });\n }\n\n /**\n * Enter edit mode for a row.\n */\n private enterEditMode(row: HTMLElement, valueEl: HTMLElement, container: HTMLElement): void {\n // Exit if already editing\n if (row.classList.contains('editing')) return;\n\n const nodeId = parseInt(row.dataset.nodeId || '0', 10);\n const widgetName = row.dataset.widgetName || '';\n const widgetType = row.dataset.widgetType || 'text';\n const rawValue = row.dataset.rawValue;\n\n // Use isNaN check instead of !nodeId to allow nodeId 0\n if (isNaN(nodeId) || !widgetName) return;\n\n // Get current constraints from the data extractor\n let constraints: any = {};\n try {\n // Try to parse constraints if they were stored\n const constraintsStr = row.dataset.constraints;\n if (constraintsStr) {\n constraints = JSON.parse(constraintsStr);\n }\n } catch (e) {\n // Ignore parse errors\n }\n\n // Create the editor\n const editorKey = `${nodeId}:${widgetName}`;\n const editor = WidgetEditorFactory.createEditor({\n nodeId,\n widgetName,\n widgetType,\n currentValue: rawValue,\n constraints,\n onChange: (value) => {\n // Update the raw value attribute for next time\n row.dataset.rawValue = String(value);\n },\n onBlur: () => {\n // Exit edit mode after a small delay (allows for clicks within editor)\n // Capture the current editor reference to avoid race condition\n const currentEditor = this.activeEditors.get(editorKey);\n setTimeout(() => {\n // Only exit if the same editor is still active (not replaced by a new one)\n if (currentEditor && this.activeEditors.get(editorKey) === currentEditor) {\n if (!container.contains(document.activeElement)) {\n this.exitEditMode(row, valueEl, container);\n }\n }\n }, 100);\n }\n });\n\n // Track the editor\n this.activeEditors.set(editorKey, editor);\n\n // Show editor, hide value\n row.classList.add('editing');\n valueEl.style.display = 'none';\n container.style.display = 'block';\n container.innerHTML = '';\n container.appendChild(editor.element);\n\n // Focus the editor\n editor.focus();\n }\n\n /**\n * Exit edit mode for a row.\n */\n private exitEditMode(row: HTMLElement, valueEl: HTMLElement, container: HTMLElement): void {\n if (!row.classList.contains('editing')) return;\n\n const nodeId = row.dataset.nodeId || '';\n const widgetName = row.dataset.widgetName || '';\n const editorKey = `${nodeId}:${widgetName}`;\n\n // Get and destroy the editor\n const editor = this.activeEditors.get(editorKey);\n if (editor) {\n // Get the actual constrained value from the widget (not the unconstrained input value)\n // This ensures displayed value matches what was actually stored\n const actualValue = WidgetSyncManager.getWidgetValue(parseInt(nodeId, 10), widgetName);\n valueEl.textContent = formatWidgetValue(actualValue ?? editor.getValue());\n editor.destroy();\n this.activeEditors.delete(editorKey);\n }\n\n // Hide editor, show value\n row.classList.remove('editing');\n valueEl.style.display = '';\n container.style.display = 'none';\n container.innerHTML = '';\n }\n\n /**\n * Attach click handlers to clickable dropdown rows.\n */\n attachDropdownClickHandlers(): void {\n if (!this.elements.content) return;\n\n const clickableRows = this.elements.content.querySelectorAll('[data-clickable]');\n clickableRows.forEach(row => {\n const clickableType = (row as HTMLElement).dataset.clickable;\n\n row.addEventListener('click', (e) => {\n e.stopPropagation();\n\n if (clickableType === 'title') {\n this.showTitleDropdown(row as HTMLElement);\n } else if (clickableType === 'execOrder') {\n this.showExecOrderDropdown(row as HTMLElement);\n } else if (clickableType === 'id') {\n this.showIdDropdown(row as HTMLElement);\n } else if (clickableType === 'zoom') {\n const nodeId = (row as HTMLElement).dataset.nodeId;\n if (nodeId) {\n const app = (window as any).app;\n const node = app.graph.getNodeById(parseInt(nodeId));\n if (node && app.canvas) {\n app.canvas.centerOnNode(node);\n }\n }\n }\n });\n\n // Add hover effect\n row.addEventListener('mouseenter', () => {\n (row as HTMLElement).style.background = 'var(--comfy-input-bg, rgba(255,255,255,0.05))';\n });\n row.addEventListener('mouseleave', () => {\n (row as HTMLElement).style.background = '';\n });\n });\n\n // Add click handlers for copy buttons\n const copyButtons = this.elements.content.querySelectorAll('.copy-btn');\n copyButtons.forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const value = (btn as HTMLElement).dataset.copyValue || '';\n navigator.clipboard.writeText(value).then(() => {\n // Show success feedback\n btn.classList.add('copied');\n const originalHtml = btn.innerHTML;\n btn.innerHTML = ``;\n setTimeout(() => {\n btn.innerHTML = originalHtml;\n btn.classList.remove('copied');\n }, 1500);\n }).catch(err => {\n console.error('Failed to copy:', err);\n });\n });\n });\n }\n\n /**\n * Update section expansion states.\n */\n updateSectionStates(): void {\n if (!this.elements.content) return;\n\n const sections = this.elements.content.querySelectorAll('.info-section');\n sections.forEach(section => {\n const sectionId = (section as HTMLElement).dataset.section;\n if (!sectionId) return;\n\n const header = section.querySelector('.section-header');\n const content = section.querySelector('.section-content');\n\n if (header && content) {\n if (this.stateManager.state.expandedSections.has(sectionId)) {\n header.classList.add('expanded');\n content.classList.add('expanded');\n } else {\n header.classList.remove('expanded');\n content.classList.remove('expanded');\n }\n }\n });\n }\n\n /**\n * Update header subtitle.\n * @param info \n */\n updateHeaderSubtitle(info: any): void {\n if (!this.elements.header) return;\n\n const subtitleElement = this.elements.header.querySelector('.header-subtitle') as HTMLElement;\n if (subtitleElement) {\n const accentColor = String(this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] || '');\n if (info.hoveredNode) {\n subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title} `;\n subtitleElement.style.color = accentColor;\n } else if (info.media) {\n subtitleElement.textContent = `Media: ${info.media.tagName} `;\n subtitleElement.style.color = accentColor;\n } else if (info.connection) {\n subtitleElement.textContent = `Connection: ${info.connection.type} `;\n subtitleElement.style.color = accentColor;\n } else {\n subtitleElement.textContent = 'Real-time analysis';\n subtitleElement.style.color = '';\n }\n }\n }\n\n /**\n * Inject CSS styles by loading external stylesheet.\n */\n injectStyles(): void {\n if (!document.getElementById('magnify-info-panel-styles-v2')) {\n const link = document.createElement('link');\n link.id = 'magnify-info-panel-styles-v2';\n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.href = 'extensions/comfyui-magnifyglass/info-panel.css';\n document.head.appendChild(link);\n }\n }\n\n /**\n * Show dropdown with nodes sorted by title.\n * @param anchorElement - Element to anchor the dropdown to\n */\n showTitleDropdown(anchorElement: HTMLElement): void {\n this.hideDropdown();\n\n const nodes = this.nodeSelector.getNodesSortedByTitle();\n if (nodes.length === 0) return;\n\n this.createDropdown(nodes, anchorElement, 'title');\n }\n\n /**\n * Show dropdown with nodes sorted by execution order.\n * @param anchorElement - Element to anchor the dropdown to\n */\n showExecOrderDropdown(anchorElement: HTMLElement): void {\n this.hideDropdown();\n\n const nodes = this.nodeSelector.getNodesSortedByExecOrder();\n if (nodes.length === 0) return;\n\n this.createDropdown(nodes, anchorElement, 'execOrder');\n }\n\n /**\n * Show dropdown with nodes sorted by ID.\n * @param anchorElement - Element to anchor the dropdown to\n */\n showIdDropdown(anchorElement: HTMLElement): void {\n this.hideDropdown();\n\n const nodes = this.nodeSelector.getNodesSortedById();\n if (nodes.length === 0) return;\n\n this.createDropdown(nodes, anchorElement, 'id');\n }\n\n /**\n * Create and show the dropdown.\n */\n private createDropdown(\n nodes: NodeListEntry[] | NodeExecOrderEntry[],\n anchorElement: HTMLElement,\n type: 'title' | 'execOrder' | 'id'\n ): void {\n const dropdown = document.createElement('div');\n dropdown.className = `node-selector-dropdown theme-${this.stateManager.state.currentTheme}`;\n dropdown.style.cssText = `\n position: absolute;\n z-index: 100000;\n max-height: 300px;\n overflow-y: auto;\n background: var(--comfy-menu-bg, #2a2a2a);\n border: 1px solid var(--border-color, #444);\n border-radius: 6px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n min-width: 200px;\n max-width: 350px;\n `;\n\n // Build dropdown items\n nodes.forEach((node) => {\n const item = document.createElement('div');\n item.className = 'dropdown-item';\n item.style.cssText = `\n padding: 8px 12px;\n cursor: pointer;\n border-bottom: 1px solid var(--border-color, #333);\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n `;\n\n // Check if this is an exec order entry\n const isExecOrder = 'order' in node;\n\n if (isExecOrder) {\n const execNode = node as NodeExecOrderEntry;\n item.innerHTML = `\n #${execNode.order}\n ${escapeHtml(execNode.title)} (#${execNode.id})\n ${escapeHtml(execNode.type)}\n `;\n } else {\n item.innerHTML = `\n ${escapeHtml(node.title)} (#${node.id})\n ${escapeHtml(node.type)}\n `;\n }\n\n // Hover effect\n item.addEventListener('mouseenter', () => {\n item.style.background = 'var(--comfy-input-bg, #3a3a3a)';\n });\n item.addEventListener('mouseleave', () => {\n item.style.background = '';\n });\n\n // Selection handler\n item.addEventListener('click', (e) => {\n e.stopPropagation();\n this.hideDropdown();\n\n // Set selected node in state\n this.stateManager.setSelectedNode(node.id);\n\n // Notify callback if set\n if (this.onNodeSelected) {\n this.onNodeSelected(node.id);\n }\n });\n\n dropdown.appendChild(item);\n });\n\n // Position dropdown below anchor\n document.body.appendChild(dropdown);\n const anchorRect = anchorElement.getBoundingClientRect();\n\n // Position below the anchor, aligned to left\n dropdown.style.top = `${anchorRect.bottom + 4}px`;\n dropdown.style.left = `${anchorRect.left}px`;\n\n // Adjust if dropdown would go off-screen\n const dropdownRect = dropdown.getBoundingClientRect();\n if (dropdownRect.right > window.innerWidth - 10) {\n dropdown.style.left = `${window.innerWidth - dropdownRect.width - 10}px`;\n }\n if (dropdownRect.bottom > window.innerHeight - 10) {\n // Show above instead\n dropdown.style.top = `${anchorRect.top - dropdownRect.height - 4}px`;\n }\n\n this.currentDropdown = dropdown;\n\n // Close on click outside\n const closeHandler = (e: MouseEvent) => {\n if (!dropdown.contains(e.target as Node) && !anchorElement.contains(e.target as Node)) {\n this.hideDropdown();\n document.removeEventListener('click', closeHandler);\n }\n };\n\n // Delay adding the listener to avoid immediate closure\n setTimeout(() => {\n document.addEventListener('click', closeHandler);\n }, 10);\n }\n\n /**\n * Hide the current dropdown.\n */\n hideDropdown(): void {\n if (this.currentDropdown && this.currentDropdown.parentNode) {\n this.currentDropdown.parentNode.removeChild(this.currentDropdown);\n this.currentDropdown = null;\n }\n }\n\n cleanup(): void {\n // Clean up active editors to prevent memory leaks\n this.cleanupEditors();\n\n if (this.elements.panel && this.elements.panel.parentNode) {\n this.elements.panel.parentNode.removeChild(this.elements.panel);\n }\n if (this.elements.controls && this.elements.controls.parentNode) {\n this.elements.controls.parentNode.removeChild(this.elements.controls);\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"UIManager.js","sources":["../../src/info-panel/UIManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel UI Manager (TypeScript)\n * \n * Complete UI Manager extracted from magnify_info_panel.js\n * Handles all DOM manipulation and UI creation for the info panel.\n */\n\nimport { StateManager } from './StateManager';\nimport { Icons } from '../shared/icons';\nimport { Logger } from '../shared/logger';\nimport { INFO_PANEL_ID } from '../shared/constants';\nimport { escapeHtml } from '../shared/utils';\nimport { formatValue, getValueClass, getValueAttributes, formatWidgetValue } from './ValueFormatter';\nimport {\n getCheckpointInfo,\n getImageInfo,\n getTextBoxContent,\n getImportantNodeParameters,\n type ImageInfoResult,\n type ParameterItem\n} from './NodeDataExtractor';\nimport { NodeSelector, type NodeListEntry, type NodeExecOrderEntry } from './NodeSelector';\nimport {\n WidgetEditorFactory,\n WidgetSyncManager,\n InlineControlFactory,\n DragValueController,\n type WidgetEditorInstance,\n type InlineControlInstance\n} from './widget-editors';\n\ninterface InfoPanelElements {\n panel: HTMLDivElement | null;\n header: HTMLDivElement | null;\n content: HTMLDivElement | null;\n controls: HTMLDivElement | null;\n}\n\n/**\n * UI Manager class.\n * Handles all DOM manipulation and UI creation.\n */\nexport class UIManager {\n stateManager: StateManager;\n elements: InfoPanelElements;\n nodeSelector: NodeSelector;\n currentDropdown: HTMLDivElement | null = null;\n onNodeSelected: ((nodeId: number) => void) | null = null;\n // Track active widget editors for cleanup\n private activeEditors: Map = new Map();\n // Track active inline controls for cleanup\n private activeInlineControls: Map = new Map();\n // Track active drag controllers for cleanup\n private activeDragControllers: Map = new Map();\n\n constructor(stateManager: StateManager) {\n this.stateManager = stateManager;\n this.elements = {\n panel: null,\n header: null,\n content: null,\n controls: null\n };\n this.nodeSelector = new NodeSelector();\n\n this.createPanel();\n this.injectStyles();\n }\n\n /**\n * Create the main panel and its components.\n */\n createPanel(): void {\n // Main panel container\n this.elements.panel = document.createElement(\"div\");\n this.elements.panel.id = INFO_PANEL_ID;\n this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`;\n\n // Header\n this.elements.header = document.createElement(\"div\");\n this.elements.header.className = \"panel-header\";\n this.elements.header.innerHTML = `\n
\n
${Icons.magnifyGlass}
\n
Inspector
\n
Real-time analysis
\n
\n
\n \n
\n `;\n\n // Content container\n this.elements.content = document.createElement(\"div\");\n this.elements.content.className = \"panel-content\";\n\n this.elements.panel.appendChild(this.elements.header);\n this.elements.panel.appendChild(this.elements.content);\n\n this.applyStyles();\n // Apply initial minimized state if set\n if (this.stateManager.state.isPanelMinimized) {\n this.elements.panel.classList.add('panel-minimized');\n this.updateMinimizedState(); // Correctly set aria-expanded state\n }\n document.body.appendChild(this.elements.panel);\n\n // Hover-expand: Add/remove 'is-expanded' class, reposition, and resize font if needed\n let originalTop: number | null = null;\n let originalFontSize: number | null = null;\n const MIN_FONT_SIZE = 10; // Minimum font size in pixels\n const MARGIN = 20; // Minimum margin from viewport edges\n\n this.elements.panel.addEventListener('mouseenter', () => {\n // Sticky Mode (Persist): disable this behavior to prevent bouncing\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n return;\n }\n\n if (!this.stateManager.state.isPanelMinimized && this.elements.panel) {\n // Save original position\n originalTop = this.elements.panel.offsetTop;\n // Save original font size just in case, though we won't change it\n originalFontSize = parseFloat(getComputedStyle(this.elements.panel).fontSize);\n\n // Add expanded class\n this.elements.panel.classList.add('is-expanded');\n\n // Simple repositioning logic only (no font scaling)\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (!this.elements.panel) return;\n\n const viewportHeight = window.innerHeight;\n const rect = this.elements.panel.getBoundingClientRect();\n\n // Position the panel to fit (move up if needed)\n if (rect.bottom > viewportHeight - MARGIN) {\n const newTop = Math.max(MARGIN, originalTop! - (rect.bottom - viewportHeight + MARGIN));\n this.elements.panel.style.top = `${newTop}px`;\n }\n });\n });\n }\n });\n\n this.elements.panel.addEventListener('mouseleave', () => {\n // Sticky Mode (Persist): disable this behavior to prevent bouncing\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n return;\n }\n\n if (this.elements.panel) {\n this.elements.panel.classList.remove('is-expanded');\n\n // Restore original position\n if (originalTop !== null) {\n this.elements.panel.style.top = `${originalTop}px`;\n originalTop = null;\n }\n\n // Restore original font size (if it was somehow changed)\n if (originalFontSize !== null) {\n this.elements.panel.style.fontSize = `${originalFontSize}px`;\n originalFontSize = null;\n }\n\n // Clear any inline styles that might have been set\n this.elements.panel.style.transform = '';\n this.elements.panel.style.transformOrigin = '';\n }\n });\n\n // Add click event delegation for panel elements\n this.elements.panel.addEventListener('click', (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n\n // Handle minimize button\n const minimizeBtn = target.closest('[data-action=\"minimize\"]');\n if (minimizeBtn) {\n this.stateManager.state.isPanelMinimized = !this.stateManager.state.isPanelMinimized;\n this.updateMinimizedState();\n return;\n }\n\n // Handle section header clicks for expand/collapse\n const sectionHeader = target.closest('.section-header') as HTMLElement;\n if (sectionHeader) {\n const sectionId = sectionHeader.getAttribute('data-section');\n // Skip node section (always expanded)\n if (sectionId && sectionId !== 'node') {\n const isExpanded = this.stateManager.state.expandedSections.has(sectionId);\n if (isExpanded) {\n this.stateManager.state.expandedSections.delete(sectionId);\n } else {\n this.stateManager.state.expandedSections.add(sectionId);\n }\n\n // Update visual state\n sectionHeader.classList.toggle('expanded', !isExpanded);\n const sectionContent = sectionHeader.nextElementSibling as HTMLElement;\n if (sectionContent && sectionContent.classList.contains('section-content')) {\n sectionContent.classList.toggle('expanded', !isExpanded);\n }\n }\n }\n });\n\n // Create floating controls after panel is in DOM\n if (!this.elements.controls) {\n this.createFloatingControls();\n // Update control states immediately after creation\n this.updateControlStates();\n }\n }\n\n /**\n * Create floating control buttons.\n */\n createFloatingControls(): void {\n this.elements.controls = document.createElement(\"div\");\n this.elements.controls.className = `floating-controls vertical-layout theme-${this.stateManager.state.currentTheme}`; // Default to vertical + theme\n\n /**\n * BUTTON MAPPING DOCUMENTATION:\n * =============================\n * Button 1 (Unlock Icon): data-action=\"pin\" → controls isPanelPinned\n * - Unpinned (default): Panel follows magnify glass\n * - Pinned: Panel stays at fixed position (unlocked from following glass)\n * \n * Button 2 (Pin Icon): data-action=\"lock\" → controls isPanelLocked\n * - Unlocked (default): Panel can be dragged\n * - Locked: Panel cannot be dragged (pinned in place)\n * \n * NOTE: The data-action names are counterintuitive but kept for backwards compatibility.\n * The ICONS correctly represent the functionality:\n * - Unlock icon = unlock from following glass\n * - Pin icon = pin position (prevent drag)\n */\n this.elements.controls.innerHTML = `\n \n \n \n \n \n \n \n \n \n \n `;\n\n // Insert before the panel in the document body, not as a child\n document.body.appendChild(this.elements.controls);\n\n // Hide and position off-screen initially - will be shown when magnify glass activates and positions them\n this.elements.controls.style.display = 'none';\n this.elements.controls.style.visibility = 'hidden';\n this.elements.controls.style.left = '-9999px';\n this.elements.controls.style.top = '-9999px';\n\n // Add click event delegation for control buttons\n this.elements.controls.addEventListener('click', (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n const button = target.closest('button[data-action]') as HTMLButtonElement;\n if (!button) return;\n\n const action = button.getAttribute('data-action');\n Logger.debug(`Control button clicked: ${action}`);\n\n switch (action) {\n case 'pin':\n this.stateManager.togglePinning();\n this.updatePinnedState();\n this.updateControlStates();\n break;\n case 'lock':\n this.stateManager.state.isPanelLocked = !this.stateManager.state.isPanelLocked;\n this.updateControlStates();\n if (this.elements.panel) {\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n }\n break;\n case 'persist':\n this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"] = !this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n this.updateControlStates();\n this.applyStyles(); // Update persist-active class\n // Sync sidebar setting if needed (optional, sidebar updates on reopen usually)\n break;\n case 'toggle-panel':\n if (this.stateManager.state.isPanelVisible) {\n // User requested to hide the panel\n this.hide();\n\n // User Request: Disable sticky info when hiding via button\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"] = false;\n this.applyStyles(); // Update visual styles if necessary\n }\n } else {\n this.show();\n }\n this.updateControlStates();\n break;\n case 'toggle-glass':\n this.stateManager.state.isGlassPreviewVisible = !this.stateManager.state.isGlassPreviewVisible;\n\n // Link Glass Visibility to Panel Pinning\n if (!this.stateManager.state.isGlassPreviewVisible) {\n // Glass Hidden\n console.log(`[MagnifyGlass Debug] Glass Hidden. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`);\n // Only auto-pin if not already pinned by the user\n if (!this.stateManager.state.isPanelPinned) {\n if (this.elements.panel) {\n const rect = this.elements.panel.getBoundingClientRect();\n this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top };\n }\n this.stateManager.state.isPanelPinned = true;\n this.stateManager.state.isAutoPinned = true; // Track that this was auto-pinned\n console.log(`[MagnifyGlass Debug] Auto-Pinned panel.`);\n }\n // Do NOT reset isPanelLocked here - preserve user's lock state\n } else {\n // Glass Shown\n console.log(`[MagnifyGlass Debug] Glass Shown. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`);\n // Only unpin if it was auto-pinned (not user-pinned)\n if (this.stateManager.state.isAutoPinned) {\n this.stateManager.state.isPanelPinned = false;\n this.stateManager.state.isAutoPinned = false;\n console.log(`[MagnifyGlass Debug] Auto-Unpinned panel.`);\n }\n // If user manually pinned, keep the panel pinned\n }\n\n this.updateControlStates();\n this.updatePinnedState(); // Update visual class\n\n // Toggle BOTH the visual visibility AND the rendering\n // This fully disables the offscreen renderer when preview is hidden\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.setGlassPreviewActive) {\n magnifyGlass.setGlassPreviewActive(this.stateManager.state.isGlassPreviewVisible);\n } else if (magnifyGlass && magnifyGlass.ui?.setPreviewVisibility) {\n // Fallback for backwards compatibility\n magnifyGlass.ui.setPreviewVisibility(this.stateManager.state.isGlassPreviewVisible);\n }\n break;\n case 'popout':\n // Toggle the pop-out tab\n const glass = (window as any).comfyUIMagnifyGlass;\n if (glass && glass.popOutManager) {\n glass.popOutManager.toggle();\n }\n break;\n case 'drag-glass':\n // Toggle glass drag mode\n const mglass = (window as any).comfyUIMagnifyGlass;\n if (mglass && mglass.state) {\n mglass.state.isDragModeEnabled = !mglass.state.isDragModeEnabled;\n this.updateControlStates();\n\n // Update UI to indicate drag mode\n if (mglass.ui && mglass.ui.setDragMode) {\n mglass.ui.setDragMode(mglass.state.isDragModeEnabled);\n }\n }\n break;\n case 'reset-glass':\n // Reset glass offsets\n const rglass = (window as any).comfyUIMagnifyGlass;\n if (rglass) {\n rglass.resetOffsets();\n Logger.debug(\"Reset glass position via hover controls\");\n\n // Disable drag mode if active\n if (rglass.state && rglass.state.isDragModeEnabled) {\n rglass.state.isDragModeEnabled = false;\n if (rglass.ui && rglass.ui.setDragMode) {\n rglass.ui.setDragMode(false);\n }\n this.updateControlStates();\n }\n }\n break;\n case 'toggle-cursor':\n const cglass = (window as any).comfyUIMagnifyGlass;\n if (cglass && cglass.config) {\n cglass.config.showCursorPreview = !cglass.config.showCursorPreview;\n cglass.updateMagnifiedView();\n this.updateControlStates();\n }\n break;\n case 'toggle-hold':\n this.stateManager.toggleHold();\n this.updateControlStates();\n break;\n }\n });\n\n // Set initial layout based on settings\n const controlsPosition = String(this.stateManager.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] || \"top-right\");\n this.updateControlsLayout(controlsPosition);\n\n // Initial state update\n this.updateControlStates();\n }\n\n /**\n * Update control button states.\n */\n updateControlStates(): void {\n if (!this.elements.controls) return;\n\n const pinBtn = this.elements.controls.querySelector('[data-action=\"pin\"]') as HTMLButtonElement;\n const lockBtn = this.elements.controls.querySelector('[data-action=\"lock\"]') as HTMLButtonElement;\n const visibilityBtn = this.elements.controls.querySelector('[data-action=\"toggle-panel\"]') as HTMLButtonElement;\n const glassBtn = this.elements.controls.querySelector('[data-action=\"toggle-glass\"]') as HTMLButtonElement;\n const isPanelVisible = this.stateManager.state.isPanelVisible;\n const isGlassVisible = this.stateManager.state.isGlassPreviewVisible;\n\n if (pinBtn) {\n pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned);\n pinBtn.setAttribute('aria-pressed', String(this.stateManager.state.isPanelPinned));\n pinBtn.title = this.stateManager.state.isPanelPinned ? \"Lock Panel\" : \"Unlock Panel\";\n pinBtn.innerHTML = this.stateManager.state.isPanelPinned ? Icons.lock : Icons.unlock;\n // Hide unlock button when panel is hidden\n pinBtn.style.display = isPanelVisible ? 'flex' : 'none';\n\n // Disable lock button when glass is hidden (no point unlocking to follow invisible glass)\n if (!isGlassVisible) {\n pinBtn.disabled = true;\n pinBtn.style.opacity = '0.5';\n pinBtn.title = \"Cannot toggle lock when glass preview is hidden\";\n } else {\n pinBtn.disabled = false;\n pinBtn.style.opacity = '';\n pinBtn.title = this.stateManager.state.isPanelPinned ? \"Lock Panel Position (Follow Glass)\" : \"Unlock Panel Position (Drag Inspector)\";\n }\n }\n\n if (lockBtn) {\n // Only show pin button when panel is visible AND unlocked from glass\n const showLockBtn = isPanelVisible && this.stateManager.state.isPanelPinned;\n lockBtn.style.display = showLockBtn ? 'flex' : 'none';\n lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked);\n lockBtn.setAttribute('aria-pressed', String(this.stateManager.state.isPanelLocked));\n lockBtn.title = this.stateManager.state.isPanelLocked ? \"Unpin Panel Position\" : \"Pin Panel Position\";\n lockBtn.disabled = !this.stateManager.state.isPanelPinned;\n }\n\n const persistBtn = this.elements.controls.querySelector('[data-action=\"persist\"]') as HTMLButtonElement;\n if (persistBtn) {\n const isPersistConfigured = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n persistBtn.classList.toggle('active', isPersistConfigured);\n persistBtn.setAttribute('aria-pressed', String(isPersistConfigured));\n persistBtn.title = isPersistConfigured ? \"Disable Sticky Info (S)\" : \"Enable Sticky Info (S)\";\n persistBtn.style.display = isPanelVisible ? 'flex' : 'none';\n }\n\n const holdBtn = this.elements.controls.querySelector('[data-action=\"toggle-hold\"]') as HTMLButtonElement;\n if (holdBtn) {\n const isPersistConfigured = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n const isHeld = this.stateManager.state.isInfoHeld;\n\n // Only show Hold button when Sticky Info (Persist) is enabled AND panel is visible\n holdBtn.style.display = (isPanelVisible && isPersistConfigured) ? 'flex' : 'none';\n\n holdBtn.classList.toggle('active', isHeld);\n holdBtn.setAttribute('aria-pressed', String(isHeld));\n // Toggle icon: Pause (to hold) vs Play (to resume)\n holdBtn.innerHTML = isHeld ? Icons.play : Icons.pause;\n holdBtn.title = isHeld ? \"Resume Info Update (P)\" : \"Pause Info Update (P)\";\n }\n\n if (visibilityBtn) {\n // Active means \"Panel is Visible\"\n visibilityBtn.classList.toggle('active', isPanelVisible);\n visibilityBtn.setAttribute('aria-pressed', String(isPanelVisible));\n visibilityBtn.title = isPanelVisible ? \"Hide Panel\" : \"Show Panel\";\n\n // Disable hide button if glass is hidden (prevent hiding everything)\n if (!isGlassVisible) {\n visibilityBtn.disabled = true;\n visibilityBtn.style.opacity = '0.5';\n visibilityBtn.title = \"Cannot hide panel when glass preview is hidden\";\n } else {\n visibilityBtn.disabled = false;\n visibilityBtn.style.opacity = '';\n }\n }\n\n if (glassBtn) {\n glassBtn.classList.toggle('active', isGlassVisible);\n glassBtn.setAttribute('aria-pressed', String(isGlassVisible));\n glassBtn.title = isGlassVisible ? \"Hide Glass Preview\" : \"Show Glass Preview\";\n\n // Only show glass toggle button if the inspector panel is visible\n glassBtn.style.display = isPanelVisible ? 'flex' : 'none';\n }\n\n const cursorBtn = this.elements.controls.querySelector('[data-action=\"toggle-cursor\"]') as HTMLButtonElement;\n if (cursorBtn) {\n const cglass = (window as any).comfyUIMagnifyGlass;\n const showCursor = cglass?.config?.showCursorPreview || false;\n cursorBtn.classList.toggle('active', showCursor);\n cursorBtn.setAttribute('aria-pressed', String(showCursor));\n cursorBtn.title = showCursor ? \"Hide Cursor Preview\" : \"Show Cursor Preview\";\n\n // Logic: Visible when glass is visible AND:\n // 1. Panel is NOT pinned (attached to glass) - original logic\n // 2. Sticky Info (Persist) is NOT enabled - user request\n const isPersistConfigured = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n\n cursorBtn.style.display = (isGlassVisible && !this.stateManager.state.isPanelPinned && !isPersistConfigured) ? 'flex' : 'none';\n }\n\n const dragGlassBtn = this.elements.controls.querySelector('[data-action=\"drag-glass\"]') as HTMLButtonElement;\n if (dragGlassBtn) {\n const mglass = (window as any).comfyUIMagnifyGlass;\n const isDragMode = mglass?.state?.isDragModeEnabled || false;\n dragGlassBtn.classList.toggle('active', isDragMode);\n dragGlassBtn.setAttribute('aria-pressed', String(isDragMode));\n dragGlassBtn.title = isDragMode ? \"Cancel Move Mode (H)\" : \"Move Glass Position (H)\";\n // Only show drag button when glass is visible (even if panel is hidden)\n dragGlassBtn.style.display = isGlassVisible ? 'flex' : 'none';\n }\n\n const resetGlassBtn = this.elements.controls.querySelector('[data-action=\"reset-glass\"]') as HTMLButtonElement;\n if (resetGlassBtn) {\n // Only show reset button when glass is visible (moves with drag/popout buttons)\n resetGlassBtn.style.display = isGlassVisible ? 'flex' : 'none';\n }\n\n const popoutBtn = this.elements.controls.querySelector('[data-action=\"popout\"]') as HTMLButtonElement;\n if (popoutBtn) {\n const glass = (window as any).comfyUIMagnifyGlass;\n const isOpen = glass?.popOutManager?.isPopOutOpen() || false;\n // Logger.debug(`[UIManager] Popout button update - Glass: ${!!glass}, IsOpen: ${isOpen}`);\n popoutBtn.classList.toggle('active', isOpen);\n popoutBtn.setAttribute('aria-pressed', String(isOpen));\n popoutBtn.title = isOpen ? \"Close Pop-out Viewer\" : \"Open Pop-out Viewer\";\n }\n }\n\n /**\n * Update controls layout based on position setting.\n * @param position \n */\n updateControlsLayout(position: string): void {\n if (!this.elements.controls) return;\n\n // Remove existing layout classes\n this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout');\n\n // Add appropriate layout class\n if (['top', 'bottom'].includes(position)) {\n this.elements.controls.classList.add('horizontal-layout');\n } else {\n this.elements.controls.classList.add('vertical-layout');\n }\n }\n\n /**\n * Load a Google Font dynamically.\n * @param fontName - Name of the font to load\n */\n loadGoogleFont(fontName: string): void {\n // Skip if already loaded\n const linkId = `google-font-${fontName.replace(/\\s+/g, '-').toLowerCase()}`;\n if (document.getElementById(linkId)) return;\n\n // Create link element for Google Fonts\n const link = document.createElement('link');\n link.id = linkId;\n link.rel = 'stylesheet';\n link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@400;500;600;700&display=swap`;\n document.head.appendChild(link);\n\n Logger.debug(`[UIManager] Loaded Google Font: ${fontName}`);\n }\n\n /**\n * Apply current styles to elements.\n */\n applyStyles(): void {\n if (!this.elements.panel) return;\n\n const settings = this.stateManager.state.settings;\n\n // Apply dimensions\n this.elements.panel.style.width = `${settings[\"🔍MagnifyGlass.InfoPanelWidth\"]}px`;\n // Set max-height via CSS variable so CSS :hover rules can override\n const maxHeight = settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"];\n this.elements.panel.style.setProperty('--panel-max-height', `${maxHeight}px`);\n this.elements.panel.style.height = 'auto';\n\n // 2. Apply static styles (only needed once or if overwritten, but safe to set)\n this.elements.panel.style.position = 'absolute';\n this.elements.panel.style.zIndex = '99999';\n this.elements.panel.style.transform = 'translateY(-10px)';\n this.elements.panel.style.pointerEvents = 'auto';\n this.elements.panel.style.userSelect = 'none';\n\n // Apply Font Family\n const fontFamily = settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"] as string || \"System Default\";\n if (fontFamily === \"System Default\" || fontFamily === \"system-ui\") {\n this.elements.panel.style.fontFamily = \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n } else if (fontFamily === \"monospace\") {\n this.elements.panel.style.fontFamily = \"'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, monospace\";\n } else {\n // Load Google Font dynamically\n this.loadGoogleFont(fontFamily);\n this.elements.panel.style.fontFamily = `'${fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;\n }\n\n // Apply Font Size\n const fontSize = settings[\"🔍MagnifyGlass.InfoPanelFontSize\"] || 14;\n this.elements.panel.style.fontSize = `${fontSize}px`;\n\n // 3. Apply transition\n this.elements.panel.style.transition = 'none';\n\n // 4. Apply Custom Colors (CSS Variables)\n const textColor = settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] as string | undefined;\n if (textColor && typeof textColor === 'string') {\n const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`;\n this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor);\n }\n\n const accentColor = settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] as string | undefined;\n if (accentColor && typeof accentColor === 'string') {\n const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`;\n this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor);\n }\n\n // 5. Apply Opacity\n // Logic: If visible, use setting. If hidden, use 0.\n // We do NOT touch 'display' here to avoid hiding it if it's currently shown.\n // 'display' is managed by show() / hide() methods exclusively.\n if (this.stateManager.state.isPanelVisible) {\n const opacityPercent = Number(settings[\"🔍MagnifyGlass.InfoPanelOpacity\"]) || 100;\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n } else {\n this.elements.panel.style.opacity = '0';\n }\n\n // 6. Apply Persist Mode Class\n const isPersist = !!settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n if (isPersist) {\n this.elements.panel.classList.add('persist-active');\n } else {\n this.elements.panel.classList.remove('persist-active');\n }\n }\n\n /**\n * Show the panel.\n */\n show(): void {\n if (!this.elements.panel) return;\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n this.elements.panel.style.display = \"block\";\n // Controls are shown by positionFloatingControls() after proper positioning\n // Apply user's opacity setting when showing (convert percentage to decimal)\n const opacityPercent = Number(this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"]) || 100;\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n // Trigger reflow\n this.elements.panel.offsetHeight;\n this.elements.panel.classList.add('visible');\n this.stateManager.state.isPanelVisible = true;\n this.updateControlStates();\n }\n }\n\n /**\n * Hide the panel.\n */\n hide(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.remove('visible');\n this.elements.panel.classList.remove('visible');\n if (this.elements.panel) {\n this.elements.panel.style.display = \"none\";\n }\n this.stateManager.state.isPanelVisible = false;\n this.updateControlStates();\n }\n\n /**\n * Update minimized state.\n */\n updateMinimizedState(): void {\n if (!this.elements.panel || !this.elements.header) return;\n\n this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized);\n const minimizeBtn = this.elements.header.querySelector('.minimize-btn');\n if (minimizeBtn) {\n minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−';\n (minimizeBtn as HTMLElement).title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel';\n minimizeBtn.setAttribute('aria-expanded', String(!this.stateManager.state.isPanelMinimized));\n }\n }\n\n /**\n * Update pinned state.\n */\n updatePinnedState(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned);\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n this.updateControlStates();\n }\n\n /**\n * Update theme class.\n * @param newTheme \n */\n updateTheme(newTheme: string): void {\n if (this.elements.panel) {\n this.elements.panel.className = this.elements.panel.className.replace(/theme-\\w+/, `theme-${newTheme.toLowerCase()}`);\n }\n if (this.elements.controls) {\n this.elements.controls.className = this.elements.controls.className.replace(/theme-\\w+/, `theme-${newTheme.toLowerCase()}`);\n }\n }\n\n /**\n * Display information in the panel.\n * @param info\n */\n displayInfo(info: any): void {\n // Skip re-rendering if user is actively editing a value\n // This prevents the editor from being destroyed mid-edit\n if (this.activeEditors.size > 0) {\n // Only update the header subtitle, don't re-render content\n this.updateHeaderSubtitle(info);\n return;\n }\n\n const sections = this.buildSections(info);\n this.renderSections(sections);\n this.updateSectionStates();\n this.updateHeaderSubtitle(info);\n }\n\n /**\n * Build section data from info object.\n * @param info \n * @returns \n */\n buildSections(info: any): any[] {\n const sections: any[] = [];\n const settings = this.stateManager.state.settings;\n\n // Inspector section\n if (settings[\"🔍MagnifyGlass.ShowInspectorTab\"] && info.cursor && info.cursor.canvas) {\n const inspectorContent = [\n { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` },\n { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` },\n { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` }\n ];\n\n sections.push({\n id: 'inspector',\n icon: Icons.info,\n title: 'Inspector',\n content: inspectorContent\n });\n }\n\n // Media section\n if (info.media) {\n const mediaContent = [\n { label: 'Type', value: info.media.tagName },\n { label: 'Source', value: info.media.src }\n ];\n\n if (info.media.naturalSize) {\n mediaContent.push({ label: 'Natural', value: info.media.naturalSize });\n }\n\n sections.push({\n id: 'media',\n icon: Icons.camera,\n title: 'Media',\n badge: info.media.tagName,\n content: mediaContent\n });\n }\n\n // Node Details section\n if (info.hoveredNode) {\n const nodeContent: any[] = []; // Initial Node details\n nodeContent.push({\n label: 'Title',\n value: `${info.hoveredNode.title || 'Untitled'} (#${info.hoveredNode.id})`,\n clickable: 'title'\n });\n if (info.hoveredNode.executionOrder !== undefined) {\n nodeContent.push({ label: 'Exec Order', value: info.hoveredNode.executionOrder, clickable: 'execOrder' });\n }\n\n // Add Zoom to Node button\n nodeContent.push({\n label: 'Location',\n value: `${Icons.focus} Focus Node`,\n clickable: 'zoom',\n nodeId: info.hoveredNode.id,\n isHtml: true\n });\n\n // Add category if available\n if (info.hoveredNode.category) {\n nodeContent.push({ label: 'Category', value: info.hoveredNode.category });\n }\n\n // Add python module path if available\n if (info.hoveredNode.pythonModule) {\n const path = info.hoveredNode.pythonModule.replace(/\\./g, '/') + '.py';\n nodeContent.push({ label: 'Path', value: path });\n }\n\n // Check if this is a complex node that shows all widgets\n const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : '';\n const isSaveNode = nodeType.includes('save') &&\n !nodeType.includes('checkpoint') &&\n !nodeType.includes('model') &&\n !nodeType.includes('preview');\n\n const showAllWidgets = info.hoveredNode.type && (\n nodeType.includes('ksampler') ||\n nodeType.includes('sampler') ||\n nodeType.includes('k_samplers') ||\n nodeType.includes('checkpoint') ||\n nodeType.includes('model') ||\n nodeType.includes('lora') ||\n nodeType.includes('controlnet') ||\n nodeType.includes('advanced') ||\n nodeType.includes('detailer') ||\n nodeType.includes('inpaint') ||\n nodeType.includes('upscale') ||\n nodeType.includes('clip') ||\n nodeType.includes('text') ||\n nodeType.includes('encode')\n ) && !isSaveNode;\n\n if (!showAllWidgets) {\n // Add specific extractions for simple nodes\n const checkpoint = getCheckpointInfo(info.hoveredNode);\n if (checkpoint) {\n nodeContent.push({ label: 'Model', value: checkpoint });\n }\n\n const imgInfo = getImageInfo(info.hoveredNode);\n if (imgInfo) {\n if (typeof imgInfo === 'string') {\n nodeContent.push({ label: 'Image', value: imgInfo });\n } else if (typeof imgInfo === 'object' && imgInfo !== null) {\n const imgResult = imgInfo as ImageInfoResult;\n nodeContent.push({ label: 'Image Size', value: `${imgResult.width}×${imgResult.height}` });\n if (imgResult.src) {\n nodeContent.push({ label: 'Image Source', value: imgResult.src });\n }\n }\n }\n\n const textContent = getTextBoxContent(info.hoveredNode);\n if (textContent) {\n nodeContent.push({ label: 'Text', value: textContent });\n }\n }\n\n const importantParameters = getImportantNodeParameters(info.hoveredNode);\n nodeContent.push(...importantParameters);\n\n sections.push({\n id: 'node',\n icon: Icons.box,\n title: 'Node',\n badge: info.hoveredNode.type,\n content: nodeContent\n });\n }\n\n return sections;\n }\n\n /**\n * Render sections to the panel.\n * @param sections \n */\n renderSections(sections: any[]): void {\n if (!this.elements.content) return;\n\n // Cleanup existing editors before re-rendering\n this.cleanupEditors();\n\n if (sections.length === 0) {\n this.elements.content.innerHTML = `\n
\n
${Icons.mapPin}
\n
Empty canvas area
\n
\n `;\n return;\n }\n\n // Check if Sticky Info is enabled (editing only works when sticky)\n const isStickyEnabled = !!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPersist\"];\n\n this.elements.content.innerHTML = sections.map(section => `\n
\n
\n ${section.icon}\n ${escapeHtml(section.title)}\n ${section.badge ? `${escapeHtml(section.badge)}` : ''}\n ${section.id !== 'node' ? `${Icons.chevronRight}` : ''}\n
\n
\n
\n ${section.content.map((item: any) => {\n const value = item.isHtml ? item.value : formatValue(item.value, item.label);\n const valueClass = getValueClass(item.value);\n const valueAttributes = getValueAttributes(item.value);\n const clickableAttr = item.clickable ? `data-clickable=\"${item.clickable}\"` : '';\n const nodeIdAttr = item.nodeId !== undefined ? `data-node-id=\"${item.nodeId}\"` : '';\n const clickableClass = item.clickable ? 'clickable-row' : '';\n\n // Editable widget detection\n const isEditable = item.isEditable && isStickyEnabled && item.widgetName && item.nodeId !== undefined;\n const editableClass = isEditable ? 'editable' : '';\n // Serialize constraints and rawValue properly (handle boolean specially to avoid \"false\" -> true coercion)\n const constraintsJson = isEditable && item.constraints ? escapeHtml(JSON.stringify(item.constraints)) : '';\n const rawValueStr = isEditable ? (typeof item.rawValue === 'boolean' ? (item.rawValue ? 'true' : 'false') : escapeHtml(String(item.rawValue ?? ''))) : '';\n const editableAttrs = isEditable ? `data-editable=\"true\" data-widget-name=\"${escapeHtml(item.widgetName)}\" data-widget-type=\"${escapeHtml(item.widgetType || 'text')}\" data-raw-value=\"${rawValueStr}\" data-constraints=\"${constraintsJson}\"` : '';\n\n // Only show dropdown arrow for actual dropdowns, not actions like zoom\n const dropdownIcon = (item.clickable && item.clickable !== 'zoom') ? '▼' : '';\n return `\n
\n ${escapeHtml(item.label)}\n ${value}${dropdownIcon}\n
\n
\n
`;\n }).join('')}\n
\n
\n
`).join('');\n\n // Add click handlers for clickable rows\n this.attachDropdownClickHandlers();\n\n // Add click handlers for editable rows (only when sticky is enabled)\n if (isStickyEnabled) {\n this.attachEditableRowHandlers();\n }\n }\n\n /**\n * Cleanup active widget editors, inline controls, and drag controllers.\n */\n private cleanupEditors(): void {\n this.activeEditors.forEach(editor => {\n try {\n editor.destroy();\n } catch (e) {\n // Ignore cleanup errors\n }\n });\n this.activeEditors.clear();\n\n this.activeInlineControls.forEach(control => {\n try {\n control.destroy();\n } catch (e) {\n // Ignore cleanup errors\n }\n });\n this.activeInlineControls.clear();\n\n this.activeDragControllers.forEach(controller => {\n try {\n controller.destroy();\n } catch (e) {\n // Ignore cleanup errors\n }\n });\n this.activeDragControllers.clear();\n }\n\n /**\n * Attach click handlers to editable widget rows.\n * Also creates inline controls for toggle/combo widgets and drag controllers.\n */\n private attachEditableRowHandlers(): void {\n if (!this.elements.content) return;\n\n const editableRows = this.elements.content.querySelectorAll('[data-editable=\"true\"]');\n editableRows.forEach(row => {\n const rowEl = row as HTMLElement;\n const valueEl = rowEl.querySelector('.info-value.original') as HTMLElement;\n const editorContainer = rowEl.querySelector('.widget-editor-container') as HTMLElement;\n const inlineContainer = rowEl.querySelector('.inline-control-container') as HTMLElement;\n\n if (!valueEl) return;\n\n const nodeId = parseInt(rowEl.dataset.nodeId || '0', 10);\n const widgetName = rowEl.dataset.widgetName || '';\n const widgetType = rowEl.dataset.widgetType || 'text';\n const rawValue = rowEl.dataset.rawValue;\n\n // Parse constraints\n let constraints: any = {};\n try {\n const constraintsStr = rowEl.dataset.constraints;\n if (constraintsStr) {\n constraints = JSON.parse(constraintsStr);\n }\n } catch (e) {\n // Ignore parse errors\n }\n\n if (isNaN(nodeId) || !widgetName) return;\n\n const controlKey = `${nodeId}:${widgetName}`;\n\n // Check if this widget type should use inline controls\n if (InlineControlFactory.shouldUseInlineControl(widgetType) && inlineContainer) {\n // Create inline control\n const control = InlineControlFactory.createControl({\n nodeId,\n widgetName,\n widgetType,\n currentValue: widgetType.toLowerCase() === 'boolean' || widgetType.toLowerCase() === 'toggle'\n ? rawValue === 'true'\n : rawValue,\n constraints,\n onChange: (value) => {\n rowEl.dataset.rawValue = String(value);\n }\n });\n\n if (control) {\n this.activeInlineControls.set(controlKey, control);\n inlineContainer.appendChild(control.element);\n inlineContainer.style.display = 'flex';\n // Hide the text value display\n valueEl.style.display = 'none';\n }\n } else if (editorContainer) {\n // Fall back to click-to-edit for text/number types\n valueEl.addEventListener('click', (e) => {\n e.stopPropagation();\n this.enterEditMode(rowEl, valueEl, editorContainer);\n });\n }\n\n // Attach drag controller if widget type supports it and we don't have an inline control\n if (DragValueController.isTypeSupported(widgetType) &&\n !InlineControlFactory.shouldUseInlineControl(widgetType)) {\n const dragController = new DragValueController(rowEl, {\n nodeId,\n widgetName,\n widgetType,\n currentValue: rawValue,\n constraints,\n onChange: (value) => {\n rowEl.dataset.rawValue = String(value);\n // Update displayed value\n valueEl.textContent = formatWidgetValue(value);\n }\n });\n this.activeDragControllers.set(controlKey, dragController);\n }\n });\n }\n\n /**\n * Enter edit mode for a row.\n */\n private enterEditMode(row: HTMLElement, valueEl: HTMLElement, container: HTMLElement): void {\n // Exit if already editing\n if (row.classList.contains('editing')) return;\n\n const nodeId = parseInt(row.dataset.nodeId || '0', 10);\n const widgetName = row.dataset.widgetName || '';\n const widgetType = row.dataset.widgetType || 'text';\n const rawValue = row.dataset.rawValue;\n\n // Use isNaN check instead of !nodeId to allow nodeId 0\n if (isNaN(nodeId) || !widgetName) return;\n\n // Get current constraints from the data extractor\n let constraints: any = {};\n try {\n // Try to parse constraints if they were stored\n const constraintsStr = row.dataset.constraints;\n if (constraintsStr) {\n constraints = JSON.parse(constraintsStr);\n }\n } catch (e) {\n // Ignore parse errors\n }\n\n // Create the editor\n const editorKey = `${nodeId}:${widgetName}`;\n const editor = WidgetEditorFactory.createEditor({\n nodeId,\n widgetName,\n widgetType,\n currentValue: rawValue,\n constraints,\n onChange: (value) => {\n // Update the raw value attribute for next time\n row.dataset.rawValue = String(value);\n },\n onBlur: () => {\n // Exit edit mode after a small delay (allows for clicks within editor)\n // Capture the current editor reference to avoid race condition\n const currentEditor = this.activeEditors.get(editorKey);\n setTimeout(() => {\n // Only exit if the same editor is still active (not replaced by a new one)\n if (currentEditor && this.activeEditors.get(editorKey) === currentEditor) {\n if (!container.contains(document.activeElement)) {\n this.exitEditMode(row, valueEl, container);\n }\n }\n }, 100);\n }\n });\n\n // Track the editor\n this.activeEditors.set(editorKey, editor);\n\n // Show editor, hide value\n row.classList.add('editing');\n valueEl.style.display = 'none';\n container.style.display = 'block';\n container.innerHTML = '';\n container.appendChild(editor.element);\n\n // Focus the editor\n editor.focus();\n }\n\n /**\n * Exit edit mode for a row.\n */\n private exitEditMode(row: HTMLElement, valueEl: HTMLElement, container: HTMLElement): void {\n if (!row.classList.contains('editing')) return;\n\n const nodeId = row.dataset.nodeId || '';\n const widgetName = row.dataset.widgetName || '';\n const editorKey = `${nodeId}:${widgetName}`;\n\n // Get and destroy the editor\n const editor = this.activeEditors.get(editorKey);\n if (editor) {\n // Get the actual constrained value from the widget (not the unconstrained input value)\n // This ensures displayed value matches what was actually stored\n const actualValue = WidgetSyncManager.getWidgetValue(parseInt(nodeId, 10), widgetName);\n valueEl.textContent = formatWidgetValue(actualValue ?? editor.getValue());\n editor.destroy();\n this.activeEditors.delete(editorKey);\n }\n\n // Hide editor, show value\n row.classList.remove('editing');\n valueEl.style.display = '';\n container.style.display = 'none';\n container.innerHTML = '';\n }\n\n /**\n * Attach click handlers to clickable dropdown rows.\n */\n attachDropdownClickHandlers(): void {\n if (!this.elements.content) return;\n\n const clickableRows = this.elements.content.querySelectorAll('[data-clickable]');\n clickableRows.forEach(row => {\n const clickableType = (row as HTMLElement).dataset.clickable;\n\n row.addEventListener('click', (e) => {\n e.stopPropagation();\n\n if (clickableType === 'title') {\n this.showTitleDropdown(row as HTMLElement);\n } else if (clickableType === 'execOrder') {\n this.showExecOrderDropdown(row as HTMLElement);\n } else if (clickableType === 'id') {\n this.showIdDropdown(row as HTMLElement);\n } else if (clickableType === 'zoom') {\n const nodeId = (row as HTMLElement).dataset.nodeId;\n if (nodeId) {\n const app = (window as any).app;\n const node = app.graph.getNodeById(parseInt(nodeId));\n if (node && app.canvas) {\n app.canvas.centerOnNode(node);\n }\n }\n }\n });\n\n // Add hover effect\n row.addEventListener('mouseenter', () => {\n (row as HTMLElement).style.background = 'var(--comfy-input-bg, rgba(255,255,255,0.05))';\n });\n row.addEventListener('mouseleave', () => {\n (row as HTMLElement).style.background = '';\n });\n });\n }\n\n /**\n * Update section expansion states.\n */\n updateSectionStates(): void {\n if (!this.elements.content) return;\n\n const sections = this.elements.content.querySelectorAll('.info-section');\n sections.forEach(section => {\n const sectionId = (section as HTMLElement).dataset.section;\n if (!sectionId) return;\n\n const header = section.querySelector('.section-header');\n const content = section.querySelector('.section-content');\n\n if (header && content) {\n if (this.stateManager.state.expandedSections.has(sectionId)) {\n header.classList.add('expanded');\n content.classList.add('expanded');\n } else {\n header.classList.remove('expanded');\n content.classList.remove('expanded');\n }\n }\n });\n }\n\n /**\n * Update header subtitle.\n * @param info \n */\n updateHeaderSubtitle(info: any): void {\n if (!this.elements.header) return;\n\n const subtitleElement = this.elements.header.querySelector('.header-subtitle') as HTMLElement;\n if (subtitleElement) {\n const accentColor = String(this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] || '');\n if (info.hoveredNode) {\n subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title} `;\n subtitleElement.style.color = accentColor;\n } else if (info.media) {\n subtitleElement.textContent = `Media: ${info.media.tagName} `;\n subtitleElement.style.color = accentColor;\n } else if (info.connection) {\n subtitleElement.textContent = `Connection: ${info.connection.type} `;\n subtitleElement.style.color = accentColor;\n } else {\n subtitleElement.textContent = 'Real-time analysis';\n subtitleElement.style.color = '';\n }\n }\n }\n\n /**\n * Inject CSS styles by loading external stylesheet.\n */\n injectStyles(): void {\n if (!document.getElementById('magnify-info-panel-styles-v2')) {\n const link = document.createElement('link');\n link.id = 'magnify-info-panel-styles-v2';\n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.href = 'extensions/comfyui-magnifyglass/info-panel.css';\n document.head.appendChild(link);\n }\n }\n\n /**\n * Show dropdown with nodes sorted by title.\n * @param anchorElement - Element to anchor the dropdown to\n */\n showTitleDropdown(anchorElement: HTMLElement): void {\n this.hideDropdown();\n\n const nodes = this.nodeSelector.getNodesSortedByTitle();\n if (nodes.length === 0) return;\n\n this.createDropdown(nodes, anchorElement, 'title');\n }\n\n /**\n * Show dropdown with nodes sorted by execution order.\n * @param anchorElement - Element to anchor the dropdown to\n */\n showExecOrderDropdown(anchorElement: HTMLElement): void {\n this.hideDropdown();\n\n const nodes = this.nodeSelector.getNodesSortedByExecOrder();\n if (nodes.length === 0) return;\n\n this.createDropdown(nodes, anchorElement, 'execOrder');\n }\n\n /**\n * Show dropdown with nodes sorted by ID.\n * @param anchorElement - Element to anchor the dropdown to\n */\n showIdDropdown(anchorElement: HTMLElement): void {\n this.hideDropdown();\n\n const nodes = this.nodeSelector.getNodesSortedById();\n if (nodes.length === 0) return;\n\n this.createDropdown(nodes, anchorElement, 'id');\n }\n\n /**\n * Create and show the dropdown.\n */\n private createDropdown(\n nodes: NodeListEntry[] | NodeExecOrderEntry[],\n anchorElement: HTMLElement,\n type: 'title' | 'execOrder' | 'id'\n ): void {\n const dropdown = document.createElement('div');\n dropdown.className = `node-selector-dropdown theme-${this.stateManager.state.currentTheme}`;\n dropdown.style.cssText = `\n position: absolute;\n z-index: 100000;\n max-height: 300px;\n overflow-y: auto;\n background: var(--comfy-menu-bg, #2a2a2a);\n border: 1px solid var(--border-color, #444);\n border-radius: 6px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n min-width: 200px;\n max-width: 350px;\n `;\n\n // Build dropdown items\n nodes.forEach((node) => {\n const item = document.createElement('div');\n item.className = 'dropdown-item';\n item.style.cssText = `\n padding: 8px 12px;\n cursor: pointer;\n border-bottom: 1px solid var(--border-color, #333);\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n `;\n\n // Check if this is an exec order entry\n const isExecOrder = 'order' in node;\n\n if (isExecOrder) {\n const execNode = node as NodeExecOrderEntry;\n item.innerHTML = `\n #${execNode.order}\n ${escapeHtml(execNode.title)} (#${execNode.id})\n ${escapeHtml(execNode.type)}\n `;\n } else {\n item.innerHTML = `\n ${escapeHtml(node.title)} (#${node.id})\n ${escapeHtml(node.type)}\n `;\n }\n\n // Hover effect\n item.addEventListener('mouseenter', () => {\n item.style.background = 'var(--comfy-input-bg, #3a3a3a)';\n });\n item.addEventListener('mouseleave', () => {\n item.style.background = '';\n });\n\n // Selection handler\n item.addEventListener('click', (e) => {\n e.stopPropagation();\n this.hideDropdown();\n\n // Set selected node in state\n this.stateManager.setSelectedNode(node.id);\n\n // Notify callback if set\n if (this.onNodeSelected) {\n this.onNodeSelected(node.id);\n }\n });\n\n dropdown.appendChild(item);\n });\n\n // Position dropdown below anchor\n document.body.appendChild(dropdown);\n const anchorRect = anchorElement.getBoundingClientRect();\n\n // Position below the anchor, aligned to left\n dropdown.style.top = `${anchorRect.bottom + 4}px`;\n dropdown.style.left = `${anchorRect.left}px`;\n\n // Adjust if dropdown would go off-screen\n const dropdownRect = dropdown.getBoundingClientRect();\n if (dropdownRect.right > window.innerWidth - 10) {\n dropdown.style.left = `${window.innerWidth - dropdownRect.width - 10}px`;\n }\n if (dropdownRect.bottom > window.innerHeight - 10) {\n // Show above instead\n dropdown.style.top = `${anchorRect.top - dropdownRect.height - 4}px`;\n }\n\n this.currentDropdown = dropdown;\n\n // Close on click outside\n const closeHandler = (e: MouseEvent) => {\n if (!dropdown.contains(e.target as Node) && !anchorElement.contains(e.target as Node)) {\n this.hideDropdown();\n document.removeEventListener('click', closeHandler);\n }\n };\n\n // Delay adding the listener to avoid immediate closure\n setTimeout(() => {\n document.addEventListener('click', closeHandler);\n }, 10);\n }\n\n /**\n * Hide the current dropdown.\n */\n hideDropdown(): void {\n if (this.currentDropdown && this.currentDropdown.parentNode) {\n this.currentDropdown.parentNode.removeChild(this.currentDropdown);\n this.currentDropdown = null;\n }\n }\n\n cleanup(): void {\n // Clean up active editors to prevent memory leaks\n this.cleanupEditors();\n\n if (this.elements.panel && this.elements.panel.parentNode) {\n this.elements.panel.parentNode.removeChild(this.elements.panel);\n }\n if (this.elements.controls && this.elements.controls.parentNode) {\n this.elements.controls.parentNode.removeChild(this.elements.controls);\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/widget-editors/DragValueController.js b/web/info-panel/widget-editors/DragValueController.js new file mode 100644 index 0000000..2ab2463 --- /dev/null +++ b/web/info-panel/widget-editors/DragValueController.js @@ -0,0 +1,197 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { WidgetSyncManager } from "./WidgetSyncManager.js"; +import { Logger } from "../../shared/logger.js"; +const DRAG_SENSITIVITY = { + // Pixels per unit change for integers + intPixelsPerUnit: 8, + // Pixels per 0.1 change for floats + floatPixelsPerTenth: 10, + // Minimum pixels to trigger option cycle for combos + comboThreshold: 40 +}; +class DragValueController { + constructor(element, config) { + __publicField(this, "element"); + __publicField(this, "config"); + __publicField(this, "isDragging", false); + __publicField(this, "startX", 0); + __publicField(this, "startValue", 0); + __publicField(this, "startOptionIndex", 0); + __publicField(this, "accumulatedDelta", 0); + // Bound event handlers for cleanup + __publicField(this, "boundPointerMove"); + __publicField(this, "boundPointerUp"); + this.element = element; + this.config = config; + this.boundPointerMove = this.onPointerMove.bind(this); + this.boundPointerUp = this.onPointerUp.bind(this); + this.init(); + } + /** + * Initialize drag handling + */ + init() { + this.element.classList.add("draggable"); + this.element.addEventListener("pointerdown", this.onPointerDown.bind(this)); + } + /** + * Add visual drag indicator to the row + */ + addDragIndicator() { + if (this.element.querySelector(".drag-indicator")) return; + const indicator = document.createElement("span"); + indicator.className = "drag-indicator"; + indicator.innerHTML = "⟷"; + indicator.setAttribute("aria-hidden", "true"); + this.element.appendChild(indicator); + } + /** + * Handle pointer down - start drag tracking + */ + onPointerDown(e) { + var _a; + if (e.button !== 0) return; + const target = e.target; + if (target.tagName === "INPUT" || target.tagName === "SELECT" || target.tagName === "BUTTON" || target.closest(".inline-control") || target.closest(".inline-control-container") || target.closest(".info-value") || target.closest(".widget-editor") || target.closest(".widget-editor-container") || target.closest(".widget-editor-stepper") || target.closest(".widget-editor-number") || target.closest(".widget-editor-input")) { + return; + } + this.isDragging = true; + this.startX = e.clientX; + this.accumulatedDelta = 0; + const type = this.config.widgetType.toLowerCase(); + if (type === "number" || type === "int" || type === "float" || type === "slider") { + this.startValue = Number(this.config.currentValue) || 0; + } else if (type === "combo") { + const options = ((_a = this.config.constraints) == null ? void 0 : _a.options) ?? []; + const currentStr = String(this.config.currentValue); + this.startOptionIndex = options.findIndex((opt) => String(opt) === currentStr); + if (this.startOptionIndex === -1) this.startOptionIndex = 0; + } + this.element.classList.add("dragging"); + this.element.setPointerCapture(e.pointerId); + document.addEventListener("pointermove", this.boundPointerMove); + document.addEventListener("pointerup", this.boundPointerUp); + e.preventDefault(); + Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`); + } + /** + * Handle pointer move - update value based on drag distance + */ + onPointerMove(e) { + if (!this.isDragging) return; + const deltaX = e.clientX - this.startX; + const type = this.config.widgetType.toLowerCase(); + if (type === "number" || type === "int" || type === "float" || type === "slider") { + this.handleNumberDrag(deltaX); + } else if (type === "combo") { + this.handleComboDrag(deltaX); + } + } + /** + * Handle number value drag + */ + handleNumberDrag(deltaX) { + var _a, _b; + const constraints = this.config.constraints; + const isInt = this.config.widgetType.toLowerCase() === "int"; + let sensitivity; + if (isInt) { + sensitivity = DRAG_SENSITIVITY.intPixelsPerUnit; + } else { + const step2 = (constraints == null ? void 0 : constraints.step) ?? 0.1; + sensitivity = DRAG_SENSITIVITY.floatPixelsPerTenth / (step2 / 0.1); + } + const units = deltaX / sensitivity; + const step = (constraints == null ? void 0 : constraints.step) ?? (isInt ? 1 : 0.1); + let newValue = this.startValue + units * step; + if ((constraints == null ? void 0 : constraints.min) !== void 0) { + newValue = Math.max(constraints.min, newValue); + } + if ((constraints == null ? void 0 : constraints.max) !== void 0) { + newValue = Math.min(constraints.max, newValue); + } + if (step) { + newValue = Math.round(newValue / step) * step; + } + if (isInt) { + newValue = Math.round(newValue); + } else { + const precision = (constraints == null ? void 0 : constraints.precision) ?? 2; + newValue = Number(newValue.toFixed(precision)); + } + WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue); + this.config.currentValue = newValue; + (_b = (_a = this.config).onChange) == null ? void 0 : _b.call(_a, newValue); + } + /** + * Handle combo value drag - cycle through options + */ + handleComboDrag(deltaX) { + var _a, _b, _c; + const options = ((_a = this.config.constraints) == null ? void 0 : _a.options) ?? []; + if (options.length === 0) return; + this.accumulatedDelta = deltaX; + const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold); + if (optionDelta > 0) { + const direction = deltaX > 0 ? 1 : -1; + let newIndex = this.startOptionIndex + optionDelta * direction; + newIndex = (newIndex % options.length + options.length) % options.length; + const newValue = options[newIndex]; + WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue); + this.config.currentValue = newValue; + (_c = (_b = this.config).onChange) == null ? void 0 : _c.call(_b, newValue); + } + } + /** + * Handle pointer up - end drag + */ + onPointerUp(e) { + if (!this.isDragging) return; + this.isDragging = false; + this.element.classList.remove("dragging"); + try { + this.element.releasePointerCapture(e.pointerId); + } catch { + } + document.removeEventListener("pointermove", this.boundPointerMove); + document.removeEventListener("pointerup", this.boundPointerUp); + Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`); + } + /** + * Update the current value (call when value changes externally) + */ + updateValue(value) { + this.config.currentValue = value; + } + /** + * Cleanup and remove drag handling + */ + destroy() { + this.element.classList.remove("draggable", "dragging"); + const indicator = this.element.querySelector(".drag-indicator"); + if (indicator) { + indicator.remove(); + } + document.removeEventListener("pointermove", this.boundPointerMove); + document.removeEventListener("pointerup", this.boundPointerUp); + } + /** + * Check if a widget type supports drag editing + */ + static isTypeSupported(widgetType) { + const type = widgetType.toLowerCase(); + return [ + "number", + "int", + "float", + "slider", + "combo" + ].includes(type); + } +} +export { + DragValueController +}; +//# sourceMappingURL=DragValueController.js.map diff --git a/web/info-panel/widget-editors/DragValueController.js.map b/web/info-panel/widget-editors/DragValueController.js.map new file mode 100644 index 0000000..6920d12 --- /dev/null +++ b/web/info-panel/widget-editors/DragValueController.js.map @@ -0,0 +1 @@ +{"version":3,"file":"DragValueController.js","sources":["../../../src/info-panel/widget-editors/DragValueController.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Drag Value Controller\n * \n * Handles horizontal drag gestures on inspector rows for quick value changes.\n * Allows users to drag left/right to adjust numbers or cycle through options.\n */\n\nimport { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';\nimport { Logger } from '../../shared/logger';\n\n/**\n * Configuration for drag behavior\n */\nexport interface DragConfig {\n nodeId: number;\n widgetName: string;\n widgetType: string;\n currentValue: unknown;\n constraints?: WidgetConstraints;\n onChange?: (value: unknown) => void;\n}\n\n/**\n * Default sensitivity settings\n */\nconst DRAG_SENSITIVITY = {\n // Pixels per unit change for integers\n intPixelsPerUnit: 8,\n // Pixels per 0.1 change for floats\n floatPixelsPerTenth: 10,\n // Minimum pixels to trigger option cycle for combos\n comboThreshold: 40,\n};\n\n/**\n * Drag Value Controller\n * Manages drag interactions for inline value editing\n */\nexport class DragValueController {\n private element: HTMLElement;\n private config: DragConfig;\n private isDragging: boolean = false;\n private startX: number = 0;\n private startValue: number = 0;\n private startOptionIndex: number = 0;\n private accumulatedDelta: number = 0;\n\n // Bound event handlers for cleanup\n private boundPointerMove: (e: PointerEvent) => void;\n private boundPointerUp: (e: PointerEvent) => void;\n\n constructor(element: HTMLElement, config: DragConfig) {\n this.element = element;\n this.config = config;\n\n this.boundPointerMove = this.onPointerMove.bind(this);\n this.boundPointerUp = this.onPointerUp.bind(this);\n\n this.init();\n }\n\n /**\n * Initialize drag handling\n */\n private init(): void {\n this.element.classList.add('draggable');\n\n this.element.addEventListener('pointerdown', this.onPointerDown.bind(this));\n\n // Add visual indicator\n // this.addDragIndicator();\n }\n\n /**\n * Add visual drag indicator to the row\n */\n private addDragIndicator(): void {\n // Check if already has indicator\n if (this.element.querySelector('.drag-indicator')) return;\n\n const indicator = document.createElement('span');\n indicator.className = 'drag-indicator';\n indicator.innerHTML = '⟷'; // Left-right arrow\n indicator.setAttribute('aria-hidden', 'true');\n this.element.appendChild(indicator);\n }\n\n /**\n * Handle pointer down - start drag tracking\n */\n private onPointerDown(e: PointerEvent): void {\n // Only handle left mouse button or touch\n if (e.button !== 0) return;\n\n // Don't start drag if clicking on interactive elements or value/editor areas\n // Only the label area should initiate drag - all controls should be clickable\n const target = e.target as HTMLElement;\n if (target.tagName === 'INPUT' ||\n target.tagName === 'SELECT' ||\n target.tagName === 'BUTTON' ||\n target.closest('.inline-control') ||\n target.closest('.inline-control-container') ||\n target.closest('.info-value') ||\n target.closest('.widget-editor') ||\n target.closest('.widget-editor-container') ||\n target.closest('.widget-editor-stepper') ||\n target.closest('.widget-editor-number') ||\n target.closest('.widget-editor-input')) {\n return;\n }\n\n this.isDragging = true;\n this.startX = e.clientX;\n this.accumulatedDelta = 0;\n\n // Capture current value\n const type = this.config.widgetType.toLowerCase();\n if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {\n this.startValue = Number(this.config.currentValue) || 0;\n } else if (type === 'combo') {\n const options = this.config.constraints?.options ?? [];\n const currentStr = String(this.config.currentValue);\n this.startOptionIndex = options.findIndex(opt => String(opt) === currentStr);\n if (this.startOptionIndex === -1) this.startOptionIndex = 0;\n }\n\n // Add dragging class\n this.element.classList.add('dragging');\n\n // Capture pointer for reliable tracking\n this.element.setPointerCapture(e.pointerId);\n\n // Add document-level listeners\n document.addEventListener('pointermove', this.boundPointerMove);\n document.addEventListener('pointerup', this.boundPointerUp);\n\n e.preventDefault();\n Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`);\n }\n\n /**\n * Handle pointer move - update value based on drag distance\n */\n private onPointerMove(e: PointerEvent): void {\n if (!this.isDragging) return;\n\n const deltaX = e.clientX - this.startX;\n const type = this.config.widgetType.toLowerCase();\n\n if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {\n this.handleNumberDrag(deltaX);\n } else if (type === 'combo') {\n this.handleComboDrag(deltaX);\n }\n }\n\n /**\n * Handle number value drag\n */\n private handleNumberDrag(deltaX: number): void {\n const constraints = this.config.constraints;\n const isInt = this.config.widgetType.toLowerCase() === 'int';\n\n // Calculate sensitivity based on type and constraints\n let sensitivity: number;\n if (isInt) {\n sensitivity = DRAG_SENSITIVITY.intPixelsPerUnit;\n } else {\n // For floats, adjust based on step\n const step = constraints?.step ?? 0.1;\n sensitivity = DRAG_SENSITIVITY.floatPixelsPerTenth / (step / 0.1);\n }\n\n // Calculate new value\n const units = deltaX / sensitivity;\n const step = constraints?.step ?? (isInt ? 1 : 0.1);\n let newValue = this.startValue + (units * step);\n\n // Apply constraints\n if (constraints?.min !== undefined) {\n newValue = Math.max(constraints.min, newValue);\n }\n if (constraints?.max !== undefined) {\n newValue = Math.min(constraints.max, newValue);\n }\n\n // Round to step precision\n if (step) {\n newValue = Math.round(newValue / step) * step;\n }\n\n // Round for display\n if (isInt) {\n newValue = Math.round(newValue);\n } else {\n const precision = constraints?.precision ?? 2;\n newValue = Number(newValue.toFixed(precision));\n }\n\n // Sync the value\n WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);\n this.config.currentValue = newValue;\n this.config.onChange?.(newValue);\n }\n\n /**\n * Handle combo value drag - cycle through options\n */\n private handleComboDrag(deltaX: number): void {\n const options = this.config.constraints?.options ?? [];\n if (options.length === 0) return;\n\n this.accumulatedDelta = deltaX;\n\n // Calculate how many options to move\n const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold);\n\n if (optionDelta > 0) {\n const direction = deltaX > 0 ? 1 : -1;\n let newIndex = this.startOptionIndex + (optionDelta * direction);\n\n // Wrap around\n newIndex = ((newIndex % options.length) + options.length) % options.length;\n\n const newValue = options[newIndex];\n WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);\n this.config.currentValue = newValue;\n this.config.onChange?.(newValue);\n }\n }\n\n /**\n * Handle pointer up - end drag\n */\n private onPointerUp(e: PointerEvent): void {\n if (!this.isDragging) return;\n\n this.isDragging = false;\n this.element.classList.remove('dragging');\n\n // Release pointer capture\n try {\n this.element.releasePointerCapture(e.pointerId);\n } catch {\n // Ignore if already released\n }\n\n // Remove document-level listeners\n document.removeEventListener('pointermove', this.boundPointerMove);\n document.removeEventListener('pointerup', this.boundPointerUp);\n\n Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`);\n }\n\n /**\n * Update the current value (call when value changes externally)\n */\n updateValue(value: unknown): void {\n this.config.currentValue = value;\n }\n\n /**\n * Cleanup and remove drag handling\n */\n destroy(): void {\n this.element.classList.remove('draggable', 'dragging');\n\n // Remove indicator\n const indicator = this.element.querySelector('.drag-indicator');\n if (indicator) {\n indicator.remove();\n }\n\n // Remove document listeners if still attached\n document.removeEventListener('pointermove', this.boundPointerMove);\n document.removeEventListener('pointerup', this.boundPointerUp);\n }\n\n /**\n * Check if a widget type supports drag editing\n */\n static isTypeSupported(widgetType: string): boolean {\n const type = widgetType.toLowerCase();\n return [\n 'number', 'int', 'float', 'slider', 'combo'\n ].includes(type);\n }\n}\n"],"names":["step"],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/widget-editors/InlineControlFactory.js b/web/info-panel/widget-editors/InlineControlFactory.js new file mode 100644 index 0000000..0436b03 --- /dev/null +++ b/web/info-panel/widget-editors/InlineControlFactory.js @@ -0,0 +1,106 @@ +import { WidgetSyncManager } from "./WidgetSyncManager.js"; +import { Logger } from "../../shared/logger.js"; +class InlineControlFactory { + /** + * Check if a widget type should use inline controls + */ + static shouldUseInlineControl(widgetType) { + const type = widgetType.toLowerCase(); + return type === "toggle" || type === "boolean" || type === "combo"; + } + /** + * Create an inline control for the given configuration + */ + static createControl(config) { + const type = config.widgetType.toLowerCase(); + switch (type) { + case "toggle": + case "boolean": + return this.createInlineToggle(config); + case "combo": + return this.createInlineDropdown(config); + default: + return null; + } + } + /** + * Create compact inline toggle for boolean values + */ + static createInlineToggle(config) { + const container = document.createElement("div"); + container.className = "inline-control inline-toggle"; + const label = document.createElement("label"); + label.className = "inline-toggle-switch"; + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.className = "inline-toggle-input"; + const boolValue = config.currentValue === true || config.currentValue === "true"; + checkbox.checked = boolValue; + const slider = document.createElement("span"); + slider.className = "inline-toggle-slider"; + container.addEventListener("click", (e) => e.stopPropagation()); + checkbox.addEventListener("change", () => { + var _a; + WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked); + (_a = config.onChange) == null ? void 0 : _a.call(config, checkbox.checked); + Logger.debug(`[InlineControl] Toggle ${config.widgetName}: ${checkbox.checked}`); + }); + label.appendChild(checkbox); + label.appendChild(slider); + container.appendChild(label); + return { + element: container, + getValue: () => checkbox.checked, + setValue: (v) => { + checkbox.checked = Boolean(v); + }, + destroy: () => { + container.remove(); + } + }; + } + /** + * Create compact inline dropdown for combo values + */ + static createInlineDropdown(config) { + var _a; + const container = document.createElement("div"); + container.className = "inline-control inline-dropdown"; + const select = document.createElement("select"); + select.className = "inline-dropdown-select"; + const options = ((_a = config.constraints) == null ? void 0 : _a.options) ?? []; + const currentValueStr = String(config.currentValue); + for (const opt of options) { + const option = document.createElement("option"); + const optStr = String(opt); + option.value = optStr; + option.textContent = optStr; + if (optStr === currentValueStr) { + option.selected = true; + } + select.appendChild(option); + } + container.addEventListener("click", (e) => e.stopPropagation()); + select.addEventListener("change", () => { + var _a2; + WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value); + (_a2 = config.onChange) == null ? void 0 : _a2.call(config, select.value); + Logger.debug(`[InlineControl] Dropdown ${config.widgetName}: ${select.value}`); + }); + container.appendChild(select); + return { + element: container, + getValue: () => select.value, + setValue: (v) => { + select.value = String(v); + }, + destroy: () => { + container.remove(); + } + }; + } +} +export { + InlineControlFactory +}; +//# sourceMappingURL=InlineControlFactory.js.map diff --git a/web/info-panel/widget-editors/InlineControlFactory.js.map b/web/info-panel/widget-editors/InlineControlFactory.js.map new file mode 100644 index 0000000..5aafc31 --- /dev/null +++ b/web/info-panel/widget-editors/InlineControlFactory.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InlineControlFactory.js","sources":["../../../src/info-panel/widget-editors/InlineControlFactory.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Inline Control Factory\n * \n * Creates compact inline controls that render directly in inspector rows\n * for immediate value editing without entering edit mode.\n */\n\nimport { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';\nimport { Logger } from '../../shared/logger';\n\n/**\n * Configuration for creating an inline control\n */\nexport interface InlineControlConfig {\n nodeId: number;\n widgetName: string;\n widgetType: string;\n currentValue: unknown;\n constraints?: WidgetConstraints;\n onChange?: (value: unknown) => void;\n}\n\n/**\n * Interface for inline control instances\n */\nexport interface InlineControlInstance {\n element: HTMLElement;\n getValue(): unknown;\n setValue(value: unknown): void;\n destroy(): void;\n}\n\n/**\n * Inline Control Factory\n * Creates compact inline controls for immediate value editing\n */\nexport class InlineControlFactory {\n /**\n * Check if a widget type should use inline controls\n */\n static shouldUseInlineControl(widgetType: string): boolean {\n const type = widgetType.toLowerCase();\n return type === 'toggle' || type === 'boolean' || type === 'combo';\n }\n\n /**\n * Create an inline control for the given configuration\n */\n static createControl(config: InlineControlConfig): InlineControlInstance | null {\n const type = config.widgetType.toLowerCase();\n\n switch (type) {\n case 'toggle':\n case 'boolean':\n return this.createInlineToggle(config);\n case 'combo':\n return this.createInlineDropdown(config);\n default:\n return null;\n }\n }\n\n /**\n * Create compact inline toggle for boolean values\n */\n private static createInlineToggle(config: InlineControlConfig): InlineControlInstance {\n const container = document.createElement('div');\n container.className = 'inline-control inline-toggle';\n\n const label = document.createElement('label');\n label.className = 'inline-toggle-switch';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'inline-toggle-input';\n // Handle boolean coercion properly\n const boolValue = config.currentValue === true || config.currentValue === 'true';\n checkbox.checked = boolValue;\n\n const slider = document.createElement('span');\n slider.className = 'inline-toggle-slider';\n\n // Stop propagation to prevent row click handlers\n container.addEventListener('click', (e) => e.stopPropagation());\n\n // Sync on change\n checkbox.addEventListener('change', () => {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked);\n config.onChange?.(checkbox.checked);\n Logger.debug(`[InlineControl] Toggle ${config.widgetName}: ${checkbox.checked}`);\n });\n\n label.appendChild(checkbox);\n label.appendChild(slider);\n container.appendChild(label);\n\n return {\n element: container,\n getValue: () => checkbox.checked,\n setValue: (v) => { checkbox.checked = Boolean(v); },\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create compact inline dropdown for combo values\n */\n private static createInlineDropdown(config: InlineControlConfig): InlineControlInstance {\n const container = document.createElement('div');\n container.className = 'inline-control inline-dropdown';\n\n const select = document.createElement('select');\n select.className = 'inline-dropdown-select';\n\n // Populate options\n const options = config.constraints?.options ?? [];\n const currentValueStr = String(config.currentValue);\n\n for (const opt of options) {\n const option = document.createElement('option');\n const optStr = String(opt);\n option.value = optStr;\n option.textContent = optStr;\n if (optStr === currentValueStr) {\n option.selected = true;\n }\n select.appendChild(option);\n }\n\n // Stop propagation to prevent row click handlers\n container.addEventListener('click', (e) => e.stopPropagation());\n\n // Sync on change\n select.addEventListener('change', () => {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value);\n config.onChange?.(select.value);\n Logger.debug(`[InlineControl] Dropdown ${config.widgetName}: ${select.value}`);\n });\n\n container.appendChild(select);\n\n return {\n element: container,\n getValue: () => select.value,\n setValue: (v) => { select.value = String(v); },\n destroy: () => { container.remove(); }\n };\n }\n}\n"],"names":["_a"],"mappings":";;AAoCO,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA,EAI9B,OAAO,uBAAuB,YAA6B;AACvD,UAAM,OAAO,WAAW,YAAA;AACxB,WAAO,SAAS,YAAY,SAAS,aAAa,SAAS;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,cAAc,QAA2D;AAC5E,UAAM,OAAO,OAAO,WAAW,YAAA;AAE/B,YAAQ,MAAA;AAAA,MACJ,KAAK;AAAA,MACL,KAAK;AACD,eAAO,KAAK,mBAAmB,MAAM;AAAA,MACzC,KAAK;AACD,eAAO,KAAK,qBAAqB,MAAM;AAAA,MAC3C;AACI,eAAO;AAAA,IAAA;AAAA,EAEnB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,mBAAmB,QAAoD;AAClF,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,YAAY;AAElB,UAAM,WAAW,SAAS,cAAc,OAAO;AAC/C,aAAS,OAAO;AAChB,aAAS,YAAY;AAErB,UAAM,YAAY,OAAO,iBAAiB,QAAQ,OAAO,iBAAiB;AAC1E,aAAS,UAAU;AAEnB,UAAM,SAAS,SAAS,cAAc,MAAM;AAC5C,WAAO,YAAY;AAGnB,cAAU,iBAAiB,SAAS,CAAC,MAAM,EAAE,iBAAiB;AAG9D,aAAS,iBAAiB,UAAU,MAAM;;AACtC,wBAAkB,gBAAgB,OAAO,QAAQ,OAAO,YAAY,SAAS,OAAO;AACpF,mBAAO,aAAP,gCAAkB,SAAS;AAC3B,aAAO,MAAM,0BAA0B,OAAO,UAAU,KAAK,SAAS,OAAO,EAAE;AAAA,IACnF,CAAC;AAED,UAAM,YAAY,QAAQ;AAC1B,UAAM,YAAY,MAAM;AACxB,cAAU,YAAY,KAAK;AAE3B,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,SAAS;AAAA,MACzB,UAAU,CAAC,MAAM;AAAE,iBAAS,UAAU,QAAQ,CAAC;AAAA,MAAG;AAAA,MAClD,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,qBAAqB,QAAoD;;AACpF,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,YAAY;AAGnB,UAAM,YAAU,YAAO,gBAAP,mBAAoB,YAAW,CAAA;AAC/C,UAAM,kBAAkB,OAAO,OAAO,YAAY;AAElD,eAAW,OAAO,SAAS;AACvB,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,YAAM,SAAS,OAAO,GAAG;AACzB,aAAO,QAAQ;AACf,aAAO,cAAc;AACrB,UAAI,WAAW,iBAAiB;AAC5B,eAAO,WAAW;AAAA,MACtB;AACA,aAAO,YAAY,MAAM;AAAA,IAC7B;AAGA,cAAU,iBAAiB,SAAS,CAAC,MAAM,EAAE,iBAAiB;AAG9D,WAAO,iBAAiB,UAAU,MAAM;;AACpC,wBAAkB,gBAAgB,OAAO,QAAQ,OAAO,YAAY,OAAO,KAAK;AAChF,OAAAA,MAAA,OAAO,aAAP,gBAAAA,IAAA,aAAkB,OAAO;AACzB,aAAO,MAAM,4BAA4B,OAAO,UAAU,KAAK,OAAO,KAAK,EAAE;AAAA,IACjF,CAAC;AAED,cAAU,YAAY,MAAM;AAE5B,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,OAAO;AAAA,MACvB,UAAU,CAAC,MAAM;AAAE,eAAO,QAAQ,OAAO,CAAC;AAAA,MAAG;AAAA,MAC7C,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AACJ;"} \ No newline at end of file diff --git a/web/info-panel/widget-editors/WidgetEditorFactory.js b/web/info-panel/widget-editors/WidgetEditorFactory.js index 94070db..000a474 100644 --- a/web/info-panel/widget-editors/WidgetEditorFactory.js +++ b/web/info-panel/widget-editors/WidgetEditorFactory.js @@ -100,7 +100,8 @@ class WidgetEditorFactory { syncValue(); }); input.addEventListener("keydown", (e) => { - if (e.key === "Escape") { + if (e.key === "Escape" || e.key === "Enter") { + e.preventDefault(); input.blur(); } }); @@ -231,7 +232,8 @@ class WidgetEditorFactory { return (_a2 = config.onBlur) == null ? void 0 : _a2.call(config); }); select.addEventListener("keydown", (e) => { - if (e.key === "Escape") { + if (e.key === "Escape" || e.key === "Enter") { + e.preventDefault(); select.blur(); } }); diff --git a/web/info-panel/widget-editors/WidgetEditorFactory.js.map b/web/info-panel/widget-editors/WidgetEditorFactory.js.map index 0935ccb..4b1ea25 100644 --- a/web/info-panel/widget-editors/WidgetEditorFactory.js.map +++ b/web/info-panel/widget-editors/WidgetEditorFactory.js.map @@ -1 +1 @@ -{"version":3,"file":"WidgetEditorFactory.js","sources":["../../../src/info-panel/widget-editors/WidgetEditorFactory.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Widget Editor Factory\n * \n * Factory class that creates the appropriate editor component for each widget type.\n * Acts as the single entry point for creating editable widget UIs in the inspector.\n */\n\nimport { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';\nimport { Logger } from '../../shared/logger';\n\n/**\n * Configuration for creating a widget editor\n */\nexport interface WidgetEditorConfig {\n nodeId: number;\n widgetName: string;\n widgetType: string;\n currentValue: unknown;\n constraints?: WidgetConstraints;\n onChange?: (value: unknown) => void;\n onFocus?: () => void;\n onBlur?: () => void;\n}\n\n/**\n * Base interface for all editor instances\n */\nexport interface WidgetEditorInstance {\n element: HTMLElement;\n getValue(): unknown;\n setValue(value: unknown): void;\n focus(): void;\n destroy(): void;\n}\n\n/**\n * Widget Editor Factory\n * Creates appropriate editor components based on widget type\n */\nexport class WidgetEditorFactory {\n /**\n * Create an editor element for the given configuration\n */\n static createEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const type = config.widgetType.toLowerCase();\n\n switch (type) {\n case 'number':\n case 'int':\n case 'float':\n return this.createNumberEditor(config);\n\n case 'combo':\n return this.createComboEditor(config);\n\n case 'toggle':\n case 'boolean':\n return this.createBooleanEditor(config);\n\n case 'slider':\n return this.createSliderEditor(config);\n\n case 'text':\n case 'string':\n case 'customtext':\n default:\n return this.createTextEditor(config);\n }\n }\n\n /**\n * Create a number input editor\n */\n private static createNumberEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-number';\n\n const input = document.createElement('input');\n input.type = 'number';\n input.className = 'widget-editor-input';\n input.value = String(config.currentValue ?? 0);\n\n // Apply constraints\n if (config.constraints) {\n if (typeof config.constraints.min === 'number') {\n input.min = String(config.constraints.min);\n }\n if (typeof config.constraints.max === 'number') {\n input.max = String(config.constraints.max);\n }\n if (typeof config.constraints.step === 'number') {\n input.step = String(config.constraints.step);\n }\n }\n\n // Create stepper buttons\n const decrementBtn = document.createElement('button');\n decrementBtn.className = 'widget-editor-stepper decrement';\n decrementBtn.innerHTML = '−';\n decrementBtn.type = 'button';\n\n const incrementBtn = document.createElement('button');\n incrementBtn.className = 'widget-editor-stepper increment';\n incrementBtn.innerHTML = '+';\n incrementBtn.type = 'button';\n\n // Event handlers\n const syncValue = () => {\n const numValue = parseFloat(input.value);\n if (!isNaN(numValue)) {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, numValue);\n config.onChange?.(numValue);\n }\n };\n\n input.addEventListener('input', syncValue);\n input.addEventListener('change', syncValue);\n input.addEventListener('focus', () => config.onFocus?.());\n input.addEventListener('blur', () => config.onBlur?.());\n\n decrementBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n const step = config.constraints?.step ?? 1;\n const currentVal = parseFloat(input.value);\n // Handle NaN (empty input) - fall back to current value or min or 0\n const baseValue = isNaN(currentVal) ? (config.constraints?.min ?? Number(config.currentValue) ?? 0) : currentVal;\n let newValue = baseValue - step;\n // Clamp to constraints\n if (config.constraints?.min !== undefined) {\n newValue = Math.max(config.constraints.min, newValue);\n }\n input.value = String(newValue);\n syncValue();\n });\n\n incrementBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n const step = config.constraints?.step ?? 1;\n const currentVal = parseFloat(input.value);\n // Handle NaN (empty input) - fall back to current value or min or 0\n const baseValue = isNaN(currentVal) ? (config.constraints?.min ?? Number(config.currentValue) ?? 0) : currentVal;\n let newValue = baseValue + step;\n // Clamp to constraints\n if (config.constraints?.max !== undefined) {\n newValue = Math.min(config.constraints.max, newValue);\n }\n input.value = String(newValue);\n syncValue();\n });\n\n // Handle keyboard\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n input.blur();\n }\n });\n\n container.appendChild(decrementBtn);\n container.appendChild(input);\n container.appendChild(incrementBtn);\n\n return {\n element: container,\n getValue: () => parseFloat(input.value),\n setValue: (v) => { input.value = String(v); },\n focus: () => { input.focus(); input.select(); },\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a text input editor\n */\n private static createTextEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const value = String(config.currentValue ?? '');\n const isLongText = value.length > 50 || value.includes('\\n');\n\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-text';\n\n let inputElement: HTMLInputElement | HTMLTextAreaElement;\n\n if (isLongText) {\n // Use textarea for long text (prompts)\n const textarea = document.createElement('textarea');\n textarea.className = 'widget-editor-textarea';\n textarea.value = value;\n textarea.rows = Math.min(8, Math.max(3, value.split('\\n').length + 1));\n inputElement = textarea;\n\n // Add character counter\n const counter = document.createElement('span');\n counter.className = 'widget-editor-counter';\n counter.textContent = `${value.length} chars`;\n container.appendChild(counter);\n\n textarea.addEventListener('input', () => {\n counter.textContent = `${textarea.value.length} chars`;\n // Auto-resize\n textarea.rows = Math.min(8, Math.max(3, textarea.value.split('\\n').length + 1));\n });\n } else {\n // Use input for short text\n const input = document.createElement('input');\n input.type = 'text';\n input.className = 'widget-editor-input';\n input.value = value;\n inputElement = input;\n }\n\n // Event handlers\n const syncValue = () => {\n WidgetSyncManager.syncWidgetValueDebounced(\n config.nodeId,\n config.widgetName,\n inputElement.value,\n 100\n );\n config.onChange?.(inputElement.value);\n };\n\n inputElement.addEventListener('input', syncValue);\n inputElement.addEventListener('focus', () => config.onFocus?.());\n inputElement.addEventListener('blur', () => config.onBlur?.());\n\n inputElement.addEventListener('keydown', (evt) => {\n const e = evt as KeyboardEvent;\n if (e.key === 'Escape') {\n inputElement.blur();\n }\n // Allow Enter in textarea, but not in input\n if (e.key === 'Enter' && inputElement instanceof HTMLInputElement) {\n inputElement.blur();\n }\n });\n\n container.insertBefore(inputElement, container.firstChild);\n\n return {\n element: container,\n getValue: () => inputElement.value,\n setValue: (v) => { inputElement.value = String(v); },\n focus: () => { inputElement.focus(); if (inputElement instanceof HTMLInputElement) inputElement.select(); },\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a combo/dropdown editor\n */\n private static createComboEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-combo';\n\n const select = document.createElement('select');\n select.className = 'widget-editor-select';\n\n // Populate options\n const options = config.constraints?.options ?? [];\n const currentValueStr = String(config.currentValue);\n for (const opt of options) {\n const option = document.createElement('option');\n const optStr = String(opt);\n option.value = optStr;\n option.textContent = optStr;\n // Use string comparison to handle non-string option types\n if (optStr === currentValueStr) {\n option.selected = true;\n }\n select.appendChild(option);\n }\n\n // Event handlers\n select.addEventListener('change', () => {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value);\n config.onChange?.(select.value);\n });\n\n select.addEventListener('focus', () => config.onFocus?.());\n select.addEventListener('blur', () => config.onBlur?.());\n\n select.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n select.blur();\n }\n });\n\n container.appendChild(select);\n\n return {\n element: container,\n getValue: () => select.value,\n setValue: (v) => { select.value = String(v); },\n focus: () => select.focus(),\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a boolean toggle editor\n */\n private static createBooleanEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-boolean';\n\n const label = document.createElement('label');\n label.className = 'widget-editor-toggle';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'widget-editor-checkbox';\n // Handle boolean coercion properly - string 'false' should be false, not true\n const boolValue = config.currentValue === true || config.currentValue === 'true';\n checkbox.checked = boolValue;\n\n const slider = document.createElement('span');\n slider.className = 'widget-editor-toggle-slider';\n\n // Event handlers\n checkbox.addEventListener('change', () => {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked);\n config.onChange?.(checkbox.checked);\n });\n\n checkbox.addEventListener('focus', () => config.onFocus?.());\n checkbox.addEventListener('blur', () => config.onBlur?.());\n\n label.appendChild(checkbox);\n label.appendChild(slider);\n container.appendChild(label);\n\n return {\n element: container,\n getValue: () => checkbox.checked,\n setValue: (v) => { checkbox.checked = Boolean(v); },\n focus: () => checkbox.focus(),\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a slider editor\n */\n private static createSliderEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-slider';\n\n const slider = document.createElement('input');\n slider.type = 'range';\n slider.className = 'widget-editor-range';\n slider.value = String(config.currentValue ?? 0);\n\n // Apply constraints\n if (config.constraints) {\n slider.min = String(config.constraints.min ?? 0);\n slider.max = String(config.constraints.max ?? 100);\n slider.step = String(config.constraints.step ?? 0.01);\n }\n\n // Value display\n const valueDisplay = document.createElement('span');\n valueDisplay.className = 'widget-editor-slider-value';\n valueDisplay.textContent = String(config.currentValue ?? 0);\n\n // Event handlers\n const syncValue = () => {\n const numValue = parseFloat(slider.value);\n valueDisplay.textContent = numValue.toFixed(config.constraints?.precision ?? 2);\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, numValue);\n config.onChange?.(numValue);\n };\n\n slider.addEventListener('input', syncValue);\n slider.addEventListener('focus', () => config.onFocus?.());\n slider.addEventListener('blur', () => config.onBlur?.());\n\n container.appendChild(slider);\n container.appendChild(valueDisplay);\n\n return {\n element: container,\n getValue: () => parseFloat(slider.value),\n setValue: (v) => {\n slider.value = String(v);\n valueDisplay.textContent = String(v);\n },\n focus: () => slider.focus(),\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Check if a widget type is supported for editing\n */\n static isTypeSupported(widgetType: string): boolean {\n const supported = [\n 'number', 'int', 'float',\n 'text', 'string', 'customtext',\n 'combo',\n 'toggle', 'boolean',\n 'slider'\n ];\n return supported.includes(widgetType.toLowerCase());\n }\n}\n"],"names":["_a"],"mappings":";AAuCO,MAAM,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAI7B,OAAO,aAAa,QAAkD;AAClE,UAAM,OAAO,OAAO,WAAW,YAAA;AAE/B,YAAQ,MAAA;AAAA,MACJ,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACD,eAAO,KAAK,mBAAmB,MAAM;AAAA,MAEzC,KAAK;AACD,eAAO,KAAK,kBAAkB,MAAM;AAAA,MAExC,KAAK;AAAA,MACL,KAAK;AACD,eAAO,KAAK,oBAAoB,MAAM;AAAA,MAE1C,KAAK;AACD,eAAO,KAAK,mBAAmB,MAAM;AAAA,MAEzC,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL;AACI,eAAO,KAAK,iBAAiB,MAAM;AAAA,IAAA;AAAA,EAE/C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,mBAAmB,QAAkD;AAChF,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,OAAO;AACb,UAAM,YAAY;AAClB,UAAM,QAAQ,OAAO,OAAO,gBAAgB,CAAC;AAG7C,QAAI,OAAO,aAAa;AACpB,UAAI,OAAO,OAAO,YAAY,QAAQ,UAAU;AAC5C,cAAM,MAAM,OAAO,OAAO,YAAY,GAAG;AAAA,MAC7C;AACA,UAAI,OAAO,OAAO,YAAY,QAAQ,UAAU;AAC5C,cAAM,MAAM,OAAO,OAAO,YAAY,GAAG;AAAA,MAC7C;AACA,UAAI,OAAO,OAAO,YAAY,SAAS,UAAU;AAC7C,cAAM,OAAO,OAAO,OAAO,YAAY,IAAI;AAAA,MAC/C;AAAA,IACJ;AAGA,UAAM,eAAe,SAAS,cAAc,QAAQ;AACpD,iBAAa,YAAY;AACzB,iBAAa,YAAY;AACzB,iBAAa,OAAO;AAEpB,UAAM,eAAe,SAAS,cAAc,QAAQ;AACpD,iBAAa,YAAY;AACzB,iBAAa,YAAY;AACzB,iBAAa,OAAO;AAGpB,UAAM,YAAY,MAAM;;AACpB,YAAM,WAAW,WAAW,MAAM,KAAK;AACvC,UAAI,CAAC,MAAM,QAAQ,GAAG;AAClB,0BAAkB,gBAAgB,OAAO,QAAQ,OAAO,YAAY,QAAQ;AAC5E,qBAAO,aAAP,gCAAkB;AAAA,MACtB;AAAA,IACJ;AAEA,UAAM,iBAAiB,SAAS,SAAS;AACzC,UAAM,iBAAiB,UAAU,SAAS;AAC1C,UAAM,iBAAiB,SAAS,MAAA;;AAAM,0BAAO,YAAP;AAAA,KAAkB;AACxD,UAAM,iBAAiB,QAAQ,MAAA;;AAAM,0BAAO,WAAP;AAAA,KAAiB;AAEtD,iBAAa,iBAAiB,SAAS,CAAC,MAAM;;AAC1C,QAAE,eAAA;AACF,QAAE,gBAAA;AACF,YAAM,SAAO,YAAO,gBAAP,mBAAoB,SAAQ;AACzC,YAAM,aAAa,WAAW,MAAM,KAAK;AAEzC,YAAM,YAAY,MAAM,UAAU,MAAK,YAAO,gBAAP,mBAAoB,QAAO,OAAO,OAAO,YAAY,KAAK,IAAK;AACtG,UAAI,WAAW,YAAY;AAE3B,YAAI,YAAO,gBAAP,mBAAoB,SAAQ,QAAW;AACvC,mBAAW,KAAK,IAAI,OAAO,YAAY,KAAK,QAAQ;AAAA,MACxD;AACA,YAAM,QAAQ,OAAO,QAAQ;AAC7B,gBAAA;AAAA,IACJ,CAAC;AAED,iBAAa,iBAAiB,SAAS,CAAC,MAAM;;AAC1C,QAAE,eAAA;AACF,QAAE,gBAAA;AACF,YAAM,SAAO,YAAO,gBAAP,mBAAoB,SAAQ;AACzC,YAAM,aAAa,WAAW,MAAM,KAAK;AAEzC,YAAM,YAAY,MAAM,UAAU,MAAK,YAAO,gBAAP,mBAAoB,QAAO,OAAO,OAAO,YAAY,KAAK,IAAK;AACtG,UAAI,WAAW,YAAY;AAE3B,YAAI,YAAO,gBAAP,mBAAoB,SAAQ,QAAW;AACvC,mBAAW,KAAK,IAAI,OAAO,YAAY,KAAK,QAAQ;AAAA,MACxD;AACA,YAAM,QAAQ,OAAO,QAAQ;AAC7B,gBAAA;AAAA,IACJ,CAAC;AAGD,UAAM,iBAAiB,WAAW,CAAC,MAAM;AACrC,UAAI,EAAE,QAAQ,UAAU;AACpB,cAAM,KAAA;AAAA,MACV;AAAA,IACJ,CAAC;AAED,cAAU,YAAY,YAAY;AAClC,cAAU,YAAY,KAAK;AAC3B,cAAU,YAAY,YAAY;AAElC,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,WAAW,MAAM,KAAK;AAAA,MACtC,UAAU,CAAC,MAAM;AAAE,cAAM,QAAQ,OAAO,CAAC;AAAA,MAAG;AAAA,MAC5C,OAAO,MAAM;AAAE,cAAM,MAAA;AAAS,cAAM,OAAA;AAAA,MAAU;AAAA,MAC9C,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,iBAAiB,QAAkD;AAC9E,UAAM,QAAQ,OAAO,OAAO,gBAAgB,EAAE;AAC9C,UAAM,aAAa,MAAM,SAAS,MAAM,MAAM,SAAS,IAAI;AAE3D,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,QAAI;AAEJ,QAAI,YAAY;AAEZ,YAAM,WAAW,SAAS,cAAc,UAAU;AAClD,eAAS,YAAY;AACrB,eAAS,QAAQ;AACjB,eAAS,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,MAAM,IAAI,EAAE,SAAS,CAAC,CAAC;AACrE,qBAAe;AAGf,YAAM,UAAU,SAAS,cAAc,MAAM;AAC7C,cAAQ,YAAY;AACpB,cAAQ,cAAc,GAAG,MAAM,MAAM;AACrC,gBAAU,YAAY,OAAO;AAE7B,eAAS,iBAAiB,SAAS,MAAM;AACrC,gBAAQ,cAAc,GAAG,SAAS,MAAM,MAAM;AAE9C,iBAAS,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,MAAM,MAAM,IAAI,EAAE,SAAS,CAAC,CAAC;AAAA,MAClF,CAAC;AAAA,IACL,OAAO;AAEH,YAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,YAAM,OAAO;AACb,YAAM,YAAY;AAClB,YAAM,QAAQ;AACd,qBAAe;AAAA,IACnB;AAGA,UAAM,YAAY,MAAM;;AACpB,wBAAkB;AAAA,QACd,OAAO;AAAA,QACP,OAAO;AAAA,QACP,aAAa;AAAA,QACb;AAAA,MAAA;AAEJ,mBAAO,aAAP,gCAAkB,aAAa;AAAA,IACnC;AAEA,iBAAa,iBAAiB,SAAS,SAAS;AAChD,iBAAa,iBAAiB,SAAS,MAAA;;AAAM,0BAAO,YAAP;AAAA,KAAkB;AAC/D,iBAAa,iBAAiB,QAAQ,MAAA;;AAAM,0BAAO,WAAP;AAAA,KAAiB;AAE7D,iBAAa,iBAAiB,WAAW,CAAC,QAAQ;AAC9C,YAAM,IAAI;AACV,UAAI,EAAE,QAAQ,UAAU;AACpB,qBAAa,KAAA;AAAA,MACjB;AAEA,UAAI,EAAE,QAAQ,WAAW,wBAAwB,kBAAkB;AAC/D,qBAAa,KAAA;AAAA,MACjB;AAAA,IACJ,CAAC;AAED,cAAU,aAAa,cAAc,UAAU,UAAU;AAEzD,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,aAAa;AAAA,MAC7B,UAAU,CAAC,MAAM;AAAE,qBAAa,QAAQ,OAAO,CAAC;AAAA,MAAG;AAAA,MACnD,OAAO,MAAM;AAAE,qBAAa,MAAA;AAAS,YAAI,wBAAwB,iBAAkB,cAAa,OAAA;AAAA,MAAU;AAAA,MAC1G,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,kBAAkB,QAAkD;;AAC/E,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,YAAY;AAGnB,UAAM,YAAU,YAAO,gBAAP,mBAAoB,YAAW,CAAA;AAC/C,UAAM,kBAAkB,OAAO,OAAO,YAAY;AAClD,eAAW,OAAO,SAAS;AACvB,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,YAAM,SAAS,OAAO,GAAG;AACzB,aAAO,QAAQ;AACf,aAAO,cAAc;AAErB,UAAI,WAAW,iBAAiB;AAC5B,eAAO,WAAW;AAAA,MACtB;AACA,aAAO,YAAY,MAAM;AAAA,IAC7B;AAGA,WAAO,iBAAiB,UAAU,MAAM;;AACpC,wBAAkB,gBAAgB,OAAO,QAAQ,OAAO,YAAY,OAAO,KAAK;AAChF,OAAAA,MAAA,OAAO,aAAP,gBAAAA,IAAA,aAAkB,OAAO;AAAA,IAC7B,CAAC;AAED,WAAO,iBAAiB,SAAS,MAAA;;AAAM,cAAAA,MAAA,OAAO,YAAP,gBAAAA,IAAA;AAAA,KAAkB;AACzD,WAAO,iBAAiB,QAAQ,MAAA;;AAAM,cAAAA,MAAA,OAAO,WAAP,gBAAAA,IAAA;AAAA,KAAiB;AAEvD,WAAO,iBAAiB,WAAW,CAAC,MAAM;AACtC,UAAI,EAAE,QAAQ,UAAU;AACpB,eAAO,KAAA;AAAA,MACX;AAAA,IACJ,CAAC;AAED,cAAU,YAAY,MAAM;AAE5B,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,OAAO;AAAA,MACvB,UAAU,CAAC,MAAM;AAAE,eAAO,QAAQ,OAAO,CAAC;AAAA,MAAG;AAAA,MAC7C,OAAO,MAAM,OAAO,MAAA;AAAA,MACpB,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,oBAAoB,QAAkD;AACjF,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,YAAY;AAElB,UAAM,WAAW,SAAS,cAAc,OAAO;AAC/C,aAAS,OAAO;AAChB,aAAS,YAAY;AAErB,UAAM,YAAY,OAAO,iBAAiB,QAAQ,OAAO,iBAAiB;AAC1E,aAAS,UAAU;AAEnB,UAAM,SAAS,SAAS,cAAc,MAAM;AAC5C,WAAO,YAAY;AAGnB,aAAS,iBAAiB,UAAU,MAAM;;AACtC,wBAAkB,gBAAgB,OAAO,QAAQ,OAAO,YAAY,SAAS,OAAO;AACpF,mBAAO,aAAP,gCAAkB,SAAS;AAAA,IAC/B,CAAC;AAED,aAAS,iBAAiB,SAAS,MAAA;;AAAM,0BAAO,YAAP;AAAA,KAAkB;AAC3D,aAAS,iBAAiB,QAAQ,MAAA;;AAAM,0BAAO,WAAP;AAAA,KAAiB;AAEzD,UAAM,YAAY,QAAQ;AAC1B,UAAM,YAAY,MAAM;AACxB,cAAU,YAAY,KAAK;AAE3B,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,SAAS;AAAA,MACzB,UAAU,CAAC,MAAM;AAAE,iBAAS,UAAU,QAAQ,CAAC;AAAA,MAAG;AAAA,MAClD,OAAO,MAAM,SAAS,MAAA;AAAA,MACtB,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,mBAAmB,QAAkD;AAChF,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAEtB,UAAM,SAAS,SAAS,cAAc,OAAO;AAC7C,WAAO,OAAO;AACd,WAAO,YAAY;AACnB,WAAO,QAAQ,OAAO,OAAO,gBAAgB,CAAC;AAG9C,QAAI,OAAO,aAAa;AACpB,aAAO,MAAM,OAAO,OAAO,YAAY,OAAO,CAAC;AAC/C,aAAO,MAAM,OAAO,OAAO,YAAY,OAAO,GAAG;AACjD,aAAO,OAAO,OAAO,OAAO,YAAY,QAAQ,IAAI;AAAA,IACxD;AAGA,UAAM,eAAe,SAAS,cAAc,MAAM;AAClD,iBAAa,YAAY;AACzB,iBAAa,cAAc,OAAO,OAAO,gBAAgB,CAAC;AAG1D,UAAM,YAAY,MAAM;;AACpB,YAAM,WAAW,WAAW,OAAO,KAAK;AACxC,mBAAa,cAAc,SAAS,UAAQ,YAAO,gBAAP,mBAAoB,cAAa,CAAC;AAC9E,wBAAkB,gBAAgB,OAAO,QAAQ,OAAO,YAAY,QAAQ;AAC5E,mBAAO,aAAP,gCAAkB;AAAA,IACtB;AAEA,WAAO,iBAAiB,SAAS,SAAS;AAC1C,WAAO,iBAAiB,SAAS,MAAA;;AAAM,0BAAO,YAAP;AAAA,KAAkB;AACzD,WAAO,iBAAiB,QAAQ,MAAA;;AAAM,0BAAO,WAAP;AAAA,KAAiB;AAEvD,cAAU,YAAY,MAAM;AAC5B,cAAU,YAAY,YAAY;AAElC,WAAO;AAAA,MACH,SAAS;AAAA,MACT,UAAU,MAAM,WAAW,OAAO,KAAK;AAAA,MACvC,UAAU,CAAC,MAAM;AACb,eAAO,QAAQ,OAAO,CAAC;AACvB,qBAAa,cAAc,OAAO,CAAC;AAAA,MACvC;AAAA,MACA,OAAO,MAAM,OAAO,MAAA;AAAA,MACpB,SAAS,MAAM;AAAE,kBAAU,OAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAE7C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,gBAAgB,YAA6B;AAChD,UAAM,YAAY;AAAA,MACd;AAAA,MAAU;AAAA,MAAO;AAAA,MACjB;AAAA,MAAQ;AAAA,MAAU;AAAA,MAClB;AAAA,MACA;AAAA,MAAU;AAAA,MACV;AAAA,IAAA;AAEJ,WAAO,UAAU,SAAS,WAAW,YAAA,CAAa;AAAA,EACtD;AACJ;"} \ No newline at end of file +{"version":3,"file":"WidgetEditorFactory.js","sources":["../../../src/info-panel/widget-editors/WidgetEditorFactory.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Widget Editor Factory\n * \n * Factory class that creates the appropriate editor component for each widget type.\n * Acts as the single entry point for creating editable widget UIs in the inspector.\n */\n\nimport { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';\nimport { Logger } from '../../shared/logger';\n\n/**\n * Configuration for creating a widget editor\n */\nexport interface WidgetEditorConfig {\n nodeId: number;\n widgetName: string;\n widgetType: string;\n currentValue: unknown;\n constraints?: WidgetConstraints;\n onChange?: (value: unknown) => void;\n onFocus?: () => void;\n onBlur?: () => void;\n}\n\n/**\n * Base interface for all editor instances\n */\nexport interface WidgetEditorInstance {\n element: HTMLElement;\n getValue(): unknown;\n setValue(value: unknown): void;\n focus(): void;\n destroy(): void;\n}\n\n/**\n * Widget Editor Factory\n * Creates appropriate editor components based on widget type\n */\nexport class WidgetEditorFactory {\n /**\n * Create an editor element for the given configuration\n */\n static createEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const type = config.widgetType.toLowerCase();\n\n switch (type) {\n case 'number':\n case 'int':\n case 'float':\n return this.createNumberEditor(config);\n\n case 'combo':\n return this.createComboEditor(config);\n\n case 'toggle':\n case 'boolean':\n return this.createBooleanEditor(config);\n\n case 'slider':\n return this.createSliderEditor(config);\n\n case 'text':\n case 'string':\n case 'customtext':\n default:\n return this.createTextEditor(config);\n }\n }\n\n /**\n * Create a number input editor\n */\n private static createNumberEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-number';\n\n const input = document.createElement('input');\n input.type = 'number';\n input.className = 'widget-editor-input';\n input.value = String(config.currentValue ?? 0);\n\n // Apply constraints\n if (config.constraints) {\n if (typeof config.constraints.min === 'number') {\n input.min = String(config.constraints.min);\n }\n if (typeof config.constraints.max === 'number') {\n input.max = String(config.constraints.max);\n }\n if (typeof config.constraints.step === 'number') {\n input.step = String(config.constraints.step);\n }\n }\n\n // Create stepper buttons\n const decrementBtn = document.createElement('button');\n decrementBtn.className = 'widget-editor-stepper decrement';\n decrementBtn.innerHTML = '−';\n decrementBtn.type = 'button';\n\n const incrementBtn = document.createElement('button');\n incrementBtn.className = 'widget-editor-stepper increment';\n incrementBtn.innerHTML = '+';\n incrementBtn.type = 'button';\n\n // Event handlers\n const syncValue = () => {\n const numValue = parseFloat(input.value);\n if (!isNaN(numValue)) {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, numValue);\n config.onChange?.(numValue);\n }\n };\n\n input.addEventListener('input', syncValue);\n input.addEventListener('change', syncValue);\n input.addEventListener('focus', () => config.onFocus?.());\n input.addEventListener('blur', () => config.onBlur?.());\n\n decrementBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n const step = config.constraints?.step ?? 1;\n const currentVal = parseFloat(input.value);\n // Handle NaN (empty input) - fall back to current value or min or 0\n const baseValue = isNaN(currentVal) ? (config.constraints?.min ?? Number(config.currentValue) ?? 0) : currentVal;\n let newValue = baseValue - step;\n // Clamp to constraints\n if (config.constraints?.min !== undefined) {\n newValue = Math.max(config.constraints.min, newValue);\n }\n input.value = String(newValue);\n syncValue();\n });\n\n incrementBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n const step = config.constraints?.step ?? 1;\n const currentVal = parseFloat(input.value);\n // Handle NaN (empty input) - fall back to current value or min or 0\n const baseValue = isNaN(currentVal) ? (config.constraints?.min ?? Number(config.currentValue) ?? 0) : currentVal;\n let newValue = baseValue + step;\n // Clamp to constraints\n if (config.constraints?.max !== undefined) {\n newValue = Math.min(config.constraints.max, newValue);\n }\n input.value = String(newValue);\n syncValue();\n });\n\n // Handle keyboard - Enter to confirm, Escape to cancel\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' || e.key === 'Enter') {\n e.preventDefault();\n input.blur();\n }\n });\n\n container.appendChild(decrementBtn);\n container.appendChild(input);\n container.appendChild(incrementBtn);\n\n return {\n element: container,\n getValue: () => parseFloat(input.value),\n setValue: (v) => { input.value = String(v); },\n focus: () => { input.focus(); input.select(); },\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a text input editor\n */\n private static createTextEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const value = String(config.currentValue ?? '');\n const isLongText = value.length > 50 || value.includes('\\n');\n\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-text';\n\n let inputElement: HTMLInputElement | HTMLTextAreaElement;\n\n if (isLongText) {\n // Use textarea for long text (prompts)\n const textarea = document.createElement('textarea');\n textarea.className = 'widget-editor-textarea';\n textarea.value = value;\n textarea.rows = Math.min(8, Math.max(3, value.split('\\n').length + 1));\n inputElement = textarea;\n\n // Add character counter\n const counter = document.createElement('span');\n counter.className = 'widget-editor-counter';\n counter.textContent = `${value.length} chars`;\n container.appendChild(counter);\n\n textarea.addEventListener('input', () => {\n counter.textContent = `${textarea.value.length} chars`;\n // Auto-resize\n textarea.rows = Math.min(8, Math.max(3, textarea.value.split('\\n').length + 1));\n });\n } else {\n // Use input for short text\n const input = document.createElement('input');\n input.type = 'text';\n input.className = 'widget-editor-input';\n input.value = value;\n inputElement = input;\n }\n\n // Event handlers\n const syncValue = () => {\n WidgetSyncManager.syncWidgetValueDebounced(\n config.nodeId,\n config.widgetName,\n inputElement.value,\n 100\n );\n config.onChange?.(inputElement.value);\n };\n\n inputElement.addEventListener('input', syncValue);\n inputElement.addEventListener('focus', () => config.onFocus?.());\n inputElement.addEventListener('blur', () => config.onBlur?.());\n\n inputElement.addEventListener('keydown', (evt) => {\n const e = evt as KeyboardEvent;\n if (e.key === 'Escape') {\n inputElement.blur();\n }\n // Allow Enter in textarea, but not in input\n if (e.key === 'Enter' && inputElement instanceof HTMLInputElement) {\n inputElement.blur();\n }\n });\n\n container.insertBefore(inputElement, container.firstChild);\n\n return {\n element: container,\n getValue: () => inputElement.value,\n setValue: (v) => { inputElement.value = String(v); },\n focus: () => { inputElement.focus(); if (inputElement instanceof HTMLInputElement) inputElement.select(); },\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a combo/dropdown editor\n */\n private static createComboEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-combo';\n\n const select = document.createElement('select');\n select.className = 'widget-editor-select';\n\n // Populate options\n const options = config.constraints?.options ?? [];\n const currentValueStr = String(config.currentValue);\n for (const opt of options) {\n const option = document.createElement('option');\n const optStr = String(opt);\n option.value = optStr;\n option.textContent = optStr;\n // Use string comparison to handle non-string option types\n if (optStr === currentValueStr) {\n option.selected = true;\n }\n select.appendChild(option);\n }\n\n // Event handlers\n select.addEventListener('change', () => {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value);\n config.onChange?.(select.value);\n });\n\n select.addEventListener('focus', () => config.onFocus?.());\n select.addEventListener('blur', () => config.onBlur?.());\n\n select.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' || e.key === 'Enter') {\n e.preventDefault();\n select.blur();\n }\n });\n\n container.appendChild(select);\n\n return {\n element: container,\n getValue: () => select.value,\n setValue: (v) => { select.value = String(v); },\n focus: () => select.focus(),\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a boolean toggle editor\n */\n private static createBooleanEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-boolean';\n\n const label = document.createElement('label');\n label.className = 'widget-editor-toggle';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'widget-editor-checkbox';\n // Handle boolean coercion properly - string 'false' should be false, not true\n const boolValue = config.currentValue === true || config.currentValue === 'true';\n checkbox.checked = boolValue;\n\n const slider = document.createElement('span');\n slider.className = 'widget-editor-toggle-slider';\n\n // Event handlers\n checkbox.addEventListener('change', () => {\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked);\n config.onChange?.(checkbox.checked);\n });\n\n checkbox.addEventListener('focus', () => config.onFocus?.());\n checkbox.addEventListener('blur', () => config.onBlur?.());\n\n label.appendChild(checkbox);\n label.appendChild(slider);\n container.appendChild(label);\n\n return {\n element: container,\n getValue: () => checkbox.checked,\n setValue: (v) => { checkbox.checked = Boolean(v); },\n focus: () => checkbox.focus(),\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Create a slider editor\n */\n private static createSliderEditor(config: WidgetEditorConfig): WidgetEditorInstance {\n const container = document.createElement('div');\n container.className = 'widget-editor widget-editor-slider';\n\n const slider = document.createElement('input');\n slider.type = 'range';\n slider.className = 'widget-editor-range';\n slider.value = String(config.currentValue ?? 0);\n\n // Apply constraints\n if (config.constraints) {\n slider.min = String(config.constraints.min ?? 0);\n slider.max = String(config.constraints.max ?? 100);\n slider.step = String(config.constraints.step ?? 0.01);\n }\n\n // Value display\n const valueDisplay = document.createElement('span');\n valueDisplay.className = 'widget-editor-slider-value';\n valueDisplay.textContent = String(config.currentValue ?? 0);\n\n // Event handlers\n const syncValue = () => {\n const numValue = parseFloat(slider.value);\n valueDisplay.textContent = numValue.toFixed(config.constraints?.precision ?? 2);\n WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, numValue);\n config.onChange?.(numValue);\n };\n\n slider.addEventListener('input', syncValue);\n slider.addEventListener('focus', () => config.onFocus?.());\n slider.addEventListener('blur', () => config.onBlur?.());\n\n container.appendChild(slider);\n container.appendChild(valueDisplay);\n\n return {\n element: container,\n getValue: () => parseFloat(slider.value),\n setValue: (v) => {\n slider.value = String(v);\n valueDisplay.textContent = String(v);\n },\n focus: () => slider.focus(),\n destroy: () => { container.remove(); }\n };\n }\n\n /**\n * Check if a widget type is supported for editing\n */\n static isTypeSupported(widgetType: string): boolean {\n const supported = [\n 'number', 'int', 'float',\n 'text', 'string', 'customtext',\n 'combo',\n 'toggle', 'boolean',\n 'slider'\n ];\n return supported.includes(widgetType.toLowerCase());\n }\n}\n"],"names":["_a"],"mappings":"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"} \ No newline at end of file