feat: add new themes and enhance popout viewer interaction
- Added 'Obsidian', 'Obsidian Dark', and 'Milk White' themes to Info Panel and PopOut viewer. - Improved theme detection in 'StateManager' to support new themes. - Implemented button widget support in PopOut viewer with 'button-click' event handling. - Enhanced widget options handling in 'PopOutManager' for combo widgets. - Updated dropdown accessibility tests.
This commit is contained in:
+99
-6
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-dark {
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96));
|
||||
background: linear-gradient(135deg, rgb(0, 0, 0), rgb(10, 10, 15));
|
||||
color: var(--info-panel-text-color, #e0e0e0);
|
||||
border: 1px solid rgba(100, 100, 120, 0.4);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light {
|
||||
background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96));
|
||||
background: linear-gradient(135deg, rgb(250, 250, 255), rgb(240, 240, 250));
|
||||
color: var(--info-panel-text-color, #2a2a2a);
|
||||
border: 1px solid rgba(200, 200, 220, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05);
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
/* Solarized Theme - warm, muted tones */
|
||||
.magnify-info-panel.theme-solarized {
|
||||
background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96));
|
||||
background: linear-gradient(135deg, rgb(0, 43, 54), rgb(7, 54, 66));
|
||||
color: var(--info-panel-text-color, #839496);
|
||||
border: 1px solid rgba(88, 110, 117, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
/* Arc Theme - modern blue-gray */
|
||||
.magnify-info-panel.theme-arc {
|
||||
background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96));
|
||||
background: linear-gradient(135deg, rgb(56, 60, 74), rgb(64, 69, 82));
|
||||
color: var(--info-panel-text-color, #d3dae3);
|
||||
border: 1px solid rgba(90, 100, 120, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15);
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
/* Nord Theme - cool arctic colors */
|
||||
.magnify-info-panel.theme-nord {
|
||||
background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96));
|
||||
background: linear-gradient(135deg, rgb(46, 52, 64), rgb(59, 66, 82));
|
||||
color: var(--info-panel-text-color, #d8dee9);
|
||||
border: 1px solid rgba(76, 86, 106, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15);
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
/* GitHub Theme - clean, professional */
|
||||
.magnify-info-panel.theme-github {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96));
|
||||
background: linear-gradient(135deg, rgb(255, 255, 255), rgb(246, 248, 250));
|
||||
color: var(--info-panel-text-color, #24292e);
|
||||
border: 1px solid rgba(225, 228, 232, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05);
|
||||
@@ -114,6 +114,37 @@
|
||||
--info-panel-accent-color: #0366d6;
|
||||
}
|
||||
|
||||
/* Obsidian Theme - deep purple/charcoal */
|
||||
.magnify-info-panel.theme-obsidian {
|
||||
background: linear-gradient(135deg, rgb(30, 30, 40), rgb(40, 40, 55));
|
||||
color: var(--info-panel-text-color, #d4d4d8);
|
||||
border: 1px solid rgba(80, 80, 100, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(12px);
|
||||
--info-panel-accent-color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Obsidian Dark Theme - even darker obsidian */
|
||||
.magnify-info-panel.theme-obsidian-dark {
|
||||
background: linear-gradient(135deg, rgb(15, 15, 20), rgb(25, 25, 35));
|
||||
color: var(--info-panel-text-color, #c4c4cc);
|
||||
border: 1px solid rgba(60, 60, 80, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 16px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(12px);
|
||||
--info-panel-accent-color: #a78bfa;
|
||||
}
|
||||
|
||||
/* Milk White Theme - soft cream/white */
|
||||
.magnify-info-panel.theme-milk-white {
|
||||
background: linear-gradient(135deg, rgb(255, 253, 250), rgb(250, 248, 245));
|
||||
color: var(--info-panel-text-color, #3d3d3d);
|
||||
border: 1px solid rgba(220, 215, 210, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 16px rgba(0, 0, 0, 0.04);
|
||||
backdrop-filter: blur(12px);
|
||||
--info-panel-accent-color: #7c3aed;
|
||||
}
|
||||
|
||||
|
||||
.magnify-info-panel.visible {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
@@ -1265,6 +1296,68 @@
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
/* Milk White Theme - light editor adjustments */
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-input,
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-textarea {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(180, 175, 170, 0.4);
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-input:focus,
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-textarea:focus {
|
||||
border-color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-select:hover {
|
||||
background-color: rgba(124, 58, 237, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-select option {
|
||||
background: #fffdfb;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-stepper {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-toggle-slider {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-toggle-slider {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-dropdown-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-dropdown-select:hover {
|
||||
background-color: rgba(124, 58, 237, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-dropdown-select option {
|
||||
background: #fffdfb;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-action-button {
|
||||
background: var(--info-panel-accent-color, #7c3aed);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Inline Controls - Compact controls that render directly in rows
|
||||
========================================================================== */
|
||||
|
||||
@@ -148,7 +148,13 @@ export class StateManager {
|
||||
// Extract theme name from various possible formats
|
||||
const paletteStr = String(colorPalette).toLowerCase();
|
||||
|
||||
if (paletteStr.includes('solarized')) {
|
||||
if (paletteStr.includes('obsidian') && paletteStr.includes('dark')) {
|
||||
detectedTheme = 'obsidian-dark';
|
||||
} else if (paletteStr.includes('obsidian')) {
|
||||
detectedTheme = 'obsidian';
|
||||
} else if (paletteStr.includes('milk') || paletteStr.includes('white')) {
|
||||
detectedTheme = 'milk-white';
|
||||
} else if (paletteStr.includes('solarized')) {
|
||||
detectedTheme = 'solarized';
|
||||
} else if (paletteStr.includes('arc')) {
|
||||
detectedTheme = 'arc';
|
||||
@@ -180,7 +186,13 @@ export class StateManager {
|
||||
|
||||
// Check for theme classes
|
||||
const classes = (body.className + ' ' + html.className).toLowerCase();
|
||||
if (classes.includes('theme-solarized') || classes.includes('solarized')) {
|
||||
if (classes.includes('theme-obsidian-dark') || classes.includes('obsidian-dark')) {
|
||||
detectedTheme = 'obsidian-dark';
|
||||
} else if (classes.includes('theme-obsidian') || classes.includes('obsidian')) {
|
||||
detectedTheme = 'obsidian';
|
||||
} else if (classes.includes('theme-milk-white') || classes.includes('milk-white') || classes.includes('milk_white')) {
|
||||
detectedTheme = 'milk-white';
|
||||
} else if (classes.includes('theme-solarized') || classes.includes('solarized')) {
|
||||
detectedTheme = 'solarized';
|
||||
} else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {
|
||||
detectedTheme = 'arc';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { WidgetSyncManager } from '../info-panel/widget-editors/WidgetSyncManage
|
||||
* Message types for BroadcastChannel communication
|
||||
*/
|
||||
interface PopOutMessage {
|
||||
type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node' | 'widget-edit';
|
||||
type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes' | 'zoom-node' | 'widget-edit' | 'button-click';
|
||||
data?: string | Partial<PopOutConfig> | PopOutInfo | NodeListData | number | WidgetEditData;
|
||||
timestamp?: number;
|
||||
}
|
||||
@@ -212,6 +212,10 @@ export class PopOutManager {
|
||||
// Popout is sending a widget value edit
|
||||
this.handleWidgetEdit(message.data as WidgetEditData);
|
||||
break;
|
||||
case 'button-click':
|
||||
// Popout is requesting a button widget callback invocation
|
||||
this.handleButtonClick(message.data as { nodeId: number; widgetName: string });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +242,41 @@ export class PopOutManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button click command from pop-out viewer.
|
||||
* Invokes the widget's callback function.
|
||||
*/
|
||||
private handleButtonClick(buttonData: { nodeId: number; widgetName: string }): void {
|
||||
if (!buttonData || typeof buttonData.nodeId !== 'number' || !buttonData.widgetName) {
|
||||
Logger.warn('[PopOut] Invalid button click data:', buttonData);
|
||||
return;
|
||||
}
|
||||
|
||||
const app = (window as any).app;
|
||||
if (!app?.graph) {
|
||||
Logger.warn('[PopOut] No app.graph available for button click');
|
||||
return;
|
||||
}
|
||||
|
||||
const node = app.graph.getNodeById(buttonData.nodeId);
|
||||
if (!node?.widgets) {
|
||||
Logger.warn(`[PopOut] Node ${buttonData.nodeId} not found or has no widgets`);
|
||||
return;
|
||||
}
|
||||
|
||||
const widget = node.widgets.find((w: any) => w.name === buttonData.widgetName);
|
||||
if (widget && typeof widget.callback === 'function') {
|
||||
try {
|
||||
widget.callback(widget.value, app.canvas, node, [0, 0], null);
|
||||
Logger.debug(`[PopOut] Button ${buttonData.widgetName} clicked successfully`);
|
||||
} catch (error) {
|
||||
Logger.warn(`[PopOut] Button callback failed:`, error);
|
||||
}
|
||||
} else {
|
||||
Logger.warn(`[PopOut] Widget ${buttonData.widgetName} not found or has no callback`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the pop-out viewer in a new tab.
|
||||
*/
|
||||
@@ -536,9 +575,24 @@ export class PopOutManager {
|
||||
safeItem.value = item.value;
|
||||
}
|
||||
|
||||
// Sanitized options for widgets
|
||||
if (item.options) {
|
||||
safeItem.options = this.sanitizeOptions(item.options);
|
||||
// Handle widget options - preserve arrays for combo dropdowns
|
||||
if (item.options !== undefined) {
|
||||
if (Array.isArray(item.options)) {
|
||||
// For combo widgets, options is an array of values - keep it as is
|
||||
safeItem.options = item.options.filter(
|
||||
(opt: any) => typeof opt !== 'function'
|
||||
);
|
||||
} else if (typeof item.options === 'object' && item.options !== null) {
|
||||
// Check for options.values (ComfyUI nested format for combo widgets)
|
||||
if (Array.isArray(item.options.values)) {
|
||||
safeItem.options = item.options.values.filter(
|
||||
(opt: any) => typeof opt !== 'function'
|
||||
);
|
||||
} else {
|
||||
// For other widgets, options is an object with config (min, max, step, etc)
|
||||
safeItem.options = this.sanitizeOptions(item.options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return safeItem;
|
||||
|
||||
@@ -82,7 +82,8 @@ describe('UIManager Dropdown Accessibility', () => {
|
||||
|
||||
it('should cleanup event listeners when item is selected', () => {
|
||||
vi.useFakeTimers();
|
||||
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
||||
const docRemoveSpy = vi.spyOn(document, 'removeEventListener');
|
||||
const winRemoveSpy = vi.spyOn(window, 'removeEventListener');
|
||||
|
||||
const nodes = [{ id: 1, title: 'Node 1', type: 'Type A' }];
|
||||
const anchor = document.createElement('div');
|
||||
@@ -98,14 +99,15 @@ describe('UIManager Dropdown Accessibility', () => {
|
||||
// Simulate click
|
||||
item.click();
|
||||
|
||||
// Assert cleanup was called
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true);
|
||||
// Assert cleanup was called - keydown is on window, mousedown on document
|
||||
expect(winRemoveSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true);
|
||||
expect(docRemoveSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should ignore keyboard events when dropdown is not focused', () => {
|
||||
it('should focus dropdown on creation for keyboard accessibility', () => {
|
||||
// Test that dropdown receives focus when created, enabling keyboard navigation
|
||||
vi.useFakeTimers();
|
||||
|
||||
const nodes = [{ id: 1, title: 'Node 1', type: 'Type A' }, { id: 2, title: 'Node 2', type: 'Type B' }];
|
||||
@@ -117,29 +119,22 @@ describe('UIManager Dropdown Accessibility', () => {
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
const dropdown = document.querySelector('.node-selector-dropdown') as HTMLElement;
|
||||
|
||||
// Dropdown should have focus for keyboard accessibility
|
||||
expect(document.activeElement).toBe(dropdown);
|
||||
|
||||
// First item should be selected by default
|
||||
const items = dropdown.querySelectorAll('.dropdown-item');
|
||||
|
||||
// Simulate focus moving elsewhere (e.g., user tabs away)
|
||||
const otherInput = document.createElement('input');
|
||||
document.body.appendChild(otherInput);
|
||||
otherInput.focus();
|
||||
expect(document.activeElement).toBe(otherInput);
|
||||
|
||||
// Arrow Down - should NOT change selection if focus is elsewhere
|
||||
const event = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true });
|
||||
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
|
||||
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
||||
expect(items[1].getAttribute('aria-selected')).toBe('false'); // Should stay on first item
|
||||
expect(items[0].getAttribute('aria-selected')).toBe('true');
|
||||
expect(items[1].getAttribute('aria-selected')).toBe('false');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should cleanup previous event listeners when opening a new dropdown via anchor click', () => {
|
||||
vi.useFakeTimers();
|
||||
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
||||
const docRemoveSpy = vi.spyOn(document, 'removeEventListener');
|
||||
const winRemoveSpy = vi.spyOn(window, 'removeEventListener');
|
||||
|
||||
const nodes = [{ id: 1, title: 'Node 1', type: 'Type A' }];
|
||||
const anchor = document.createElement('div');
|
||||
@@ -149,15 +144,16 @@ describe('UIManager Dropdown Accessibility', () => {
|
||||
(uiManager as any).createDropdown(nodes, anchor, 'title');
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// Reset spy to track calls for the next action
|
||||
removeEventListenerSpy.mockClear();
|
||||
// Reset spies to track calls for the next action
|
||||
docRemoveSpy.mockClear();
|
||||
winRemoveSpy.mockClear();
|
||||
|
||||
// Simulate re-opening (which calls hideDropdown then createDropdown)
|
||||
(uiManager as any).showTitleDropdown(anchor);
|
||||
|
||||
// Verify that cleanup for the FIRST dropdown occurred
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true);
|
||||
expect(winRemoveSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true);
|
||||
expect(docRemoveSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
+99
-6
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-dark {
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96));
|
||||
background: linear-gradient(135deg, rgb(0, 0, 0), rgb(10, 10, 15));
|
||||
color: var(--info-panel-text-color, #e0e0e0);
|
||||
border: 1px solid rgba(100, 100, 120, 0.4);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light {
|
||||
background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96));
|
||||
background: linear-gradient(135deg, rgb(250, 250, 255), rgb(240, 240, 250));
|
||||
color: var(--info-panel-text-color, #2a2a2a);
|
||||
border: 1px solid rgba(200, 200, 220, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05);
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
/* Solarized Theme - warm, muted tones */
|
||||
.magnify-info-panel.theme-solarized {
|
||||
background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96));
|
||||
background: linear-gradient(135deg, rgb(0, 43, 54), rgb(7, 54, 66));
|
||||
color: var(--info-panel-text-color, #839496);
|
||||
border: 1px solid rgba(88, 110, 117, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
/* Arc Theme - modern blue-gray */
|
||||
.magnify-info-panel.theme-arc {
|
||||
background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96));
|
||||
background: linear-gradient(135deg, rgb(56, 60, 74), rgb(64, 69, 82));
|
||||
color: var(--info-panel-text-color, #d3dae3);
|
||||
border: 1px solid rgba(90, 100, 120, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15);
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
/* Nord Theme - cool arctic colors */
|
||||
.magnify-info-panel.theme-nord {
|
||||
background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96));
|
||||
background: linear-gradient(135deg, rgb(46, 52, 64), rgb(59, 66, 82));
|
||||
color: var(--info-panel-text-color, #d8dee9);
|
||||
border: 1px solid rgba(76, 86, 106, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15);
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
/* GitHub Theme - clean, professional */
|
||||
.magnify-info-panel.theme-github {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96));
|
||||
background: linear-gradient(135deg, rgb(255, 255, 255), rgb(246, 248, 250));
|
||||
color: var(--info-panel-text-color, #24292e);
|
||||
border: 1px solid rgba(225, 228, 232, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05);
|
||||
@@ -114,6 +114,37 @@
|
||||
--info-panel-accent-color: #0366d6;
|
||||
}
|
||||
|
||||
/* Obsidian Theme - deep purple/charcoal */
|
||||
.magnify-info-panel.theme-obsidian {
|
||||
background: linear-gradient(135deg, rgb(30, 30, 40), rgb(40, 40, 55));
|
||||
color: var(--info-panel-text-color, #d4d4d8);
|
||||
border: 1px solid rgba(80, 80, 100, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(12px);
|
||||
--info-panel-accent-color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Obsidian Dark Theme - even darker obsidian */
|
||||
.magnify-info-panel.theme-obsidian-dark {
|
||||
background: linear-gradient(135deg, rgb(15, 15, 20), rgb(25, 25, 35));
|
||||
color: var(--info-panel-text-color, #c4c4cc);
|
||||
border: 1px solid rgba(60, 60, 80, 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 16px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(12px);
|
||||
--info-panel-accent-color: #a78bfa;
|
||||
}
|
||||
|
||||
/* Milk White Theme - soft cream/white */
|
||||
.magnify-info-panel.theme-milk-white {
|
||||
background: linear-gradient(135deg, rgb(255, 253, 250), rgb(250, 248, 245));
|
||||
color: var(--info-panel-text-color, #3d3d3d);
|
||||
border: 1px solid rgba(220, 215, 210, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 16px rgba(0, 0, 0, 0.04);
|
||||
backdrop-filter: blur(12px);
|
||||
--info-panel-accent-color: #7c3aed;
|
||||
}
|
||||
|
||||
|
||||
.magnify-info-panel.visible {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
@@ -1265,6 +1296,68 @@
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
/* Milk White Theme - light editor adjustments */
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-input,
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-textarea {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(180, 175, 170, 0.4);
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-input:focus,
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-textarea:focus {
|
||||
border-color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-select:hover {
|
||||
background-color: rgba(124, 58, 237, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-select option {
|
||||
background: #fffdfb;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-stepper {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .widget-editor-toggle-slider {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-toggle-slider {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-dropdown-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-dropdown-select:hover {
|
||||
background-color: rgba(124, 58, 237, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-dropdown-select option {
|
||||
background: #fffdfb;
|
||||
color: var(--info-panel-accent-color, #7c3aed);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-milk-white .inline-action-button {
|
||||
background: var(--info-panel-accent-color, #7c3aed);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Inline Controls - Compact controls that render directly in rows
|
||||
========================================================================== */
|
||||
|
||||
@@ -71,7 +71,13 @@ class StateManager {
|
||||
const colorPalette = getSettingValue("Comfy.ColorPalette", "");
|
||||
if (colorPalette) {
|
||||
const paletteStr = String(colorPalette).toLowerCase();
|
||||
if (paletteStr.includes("solarized")) {
|
||||
if (paletteStr.includes("obsidian") && paletteStr.includes("dark")) {
|
||||
detectedTheme = "obsidian-dark";
|
||||
} else if (paletteStr.includes("obsidian")) {
|
||||
detectedTheme = "obsidian";
|
||||
} else if (paletteStr.includes("milk") || paletteStr.includes("white")) {
|
||||
detectedTheme = "milk-white";
|
||||
} else if (paletteStr.includes("solarized")) {
|
||||
detectedTheme = "solarized";
|
||||
} else if (paletteStr.includes("arc")) {
|
||||
detectedTheme = "arc";
|
||||
@@ -96,7 +102,13 @@ class StateManager {
|
||||
detectedTheme = dataTheme.toLowerCase();
|
||||
}
|
||||
const classes = (body.className + " " + html.className).toLowerCase();
|
||||
if (classes.includes("theme-solarized") || classes.includes("solarized")) {
|
||||
if (classes.includes("theme-obsidian-dark") || classes.includes("obsidian-dark")) {
|
||||
detectedTheme = "obsidian-dark";
|
||||
} else if (classes.includes("theme-obsidian") || classes.includes("obsidian")) {
|
||||
detectedTheme = "obsidian";
|
||||
} else if (classes.includes("theme-milk-white") || classes.includes("milk-white") || classes.includes("milk_white")) {
|
||||
detectedTheme = "milk-white";
|
||||
} else if (classes.includes("theme-solarized") || classes.includes("solarized")) {
|
||||
detectedTheme = "solarized";
|
||||
} else if (classes.includes("theme-arc") || classes.includes("arc-theme")) {
|
||||
detectedTheme = "arc";
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -110,6 +110,9 @@ class PopOutManager {
|
||||
case "widget-edit":
|
||||
this.handleWidgetEdit(message.data);
|
||||
break;
|
||||
case "button-click":
|
||||
this.handleButtonClick(message.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -132,6 +135,37 @@ class PopOutManager {
|
||||
Logger.warn(`[PopOut] Widget edit failed: ${result.error}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle button click command from pop-out viewer.
|
||||
* Invokes the widget's callback function.
|
||||
*/
|
||||
handleButtonClick(buttonData) {
|
||||
if (!buttonData || typeof buttonData.nodeId !== "number" || !buttonData.widgetName) {
|
||||
Logger.warn("[PopOut] Invalid button click data:", buttonData);
|
||||
return;
|
||||
}
|
||||
const app = window.app;
|
||||
if (!(app == null ? void 0 : app.graph)) {
|
||||
Logger.warn("[PopOut] No app.graph available for button click");
|
||||
return;
|
||||
}
|
||||
const node = app.graph.getNodeById(buttonData.nodeId);
|
||||
if (!(node == null ? void 0 : node.widgets)) {
|
||||
Logger.warn(`[PopOut] Node ${buttonData.nodeId} not found or has no widgets`);
|
||||
return;
|
||||
}
|
||||
const widget = node.widgets.find((w) => w.name === buttonData.widgetName);
|
||||
if (widget && typeof widget.callback === "function") {
|
||||
try {
|
||||
widget.callback(widget.value, app.canvas, node, [0, 0], null);
|
||||
Logger.debug(`[PopOut] Button ${buttonData.widgetName} clicked successfully`);
|
||||
} catch (error) {
|
||||
Logger.warn(`[PopOut] Button callback failed:`, error);
|
||||
}
|
||||
} else {
|
||||
Logger.warn(`[PopOut] Widget ${buttonData.widgetName} not found or has no callback`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Open the pop-out viewer in a new tab.
|
||||
*/
|
||||
@@ -360,8 +394,20 @@ class PopOutManager {
|
||||
if (item.value !== void 0 && typeof item.value !== "function") {
|
||||
safeItem.value = item.value;
|
||||
}
|
||||
if (item.options) {
|
||||
safeItem.options = this.sanitizeOptions(item.options);
|
||||
if (item.options !== void 0) {
|
||||
if (Array.isArray(item.options)) {
|
||||
safeItem.options = item.options.filter(
|
||||
(opt) => typeof opt !== "function"
|
||||
);
|
||||
} else if (typeof item.options === "object" && item.options !== null) {
|
||||
if (Array.isArray(item.options.values)) {
|
||||
safeItem.options = item.options.values.filter(
|
||||
(opt) => typeof opt !== "function"
|
||||
);
|
||||
} else {
|
||||
safeItem.options = this.sanitizeOptions(item.options);
|
||||
}
|
||||
}
|
||||
}
|
||||
return safeItem;
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
+242
-40
@@ -157,6 +157,54 @@
|
||||
--accent-green: #a6e22e;
|
||||
}
|
||||
|
||||
/* Obsidian Theme - deep purple/charcoal */
|
||||
[data-theme="obsidian"] {
|
||||
--bg-primary: rgb(30, 30, 40);
|
||||
--bg-secondary: rgb(40, 40, 55);
|
||||
--bg-tertiary: rgb(50, 50, 65);
|
||||
--bg-elevated: rgb(45, 45, 60);
|
||||
--border-subtle: rgba(80, 80, 100, 0.3);
|
||||
--border-default: rgba(80, 80, 100, 0.5);
|
||||
--text-primary: #d4d4d8;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-tertiary: #71717a;
|
||||
--accent-blue: #8b5cf6;
|
||||
--accent-green: #a78bfa;
|
||||
--accent-purple: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Obsidian Dark Theme - even darker obsidian */
|
||||
[data-theme="obsidian-dark"] {
|
||||
--bg-primary: rgb(15, 15, 20);
|
||||
--bg-secondary: rgb(25, 25, 35);
|
||||
--bg-tertiary: rgb(35, 35, 45);
|
||||
--bg-elevated: rgb(30, 30, 40);
|
||||
--border-subtle: rgba(60, 60, 80, 0.3);
|
||||
--border-default: rgba(60, 60, 80, 0.5);
|
||||
--text-primary: #c4c4cc;
|
||||
--text-secondary: #9494a0;
|
||||
--text-tertiary: #64647a;
|
||||
--accent-blue: #a78bfa;
|
||||
--accent-green: #c4b5fd;
|
||||
--accent-purple: #a78bfa;
|
||||
}
|
||||
|
||||
/* Milk White Theme - soft cream/white */
|
||||
[data-theme="milk-white"] {
|
||||
--bg-primary: rgb(255, 253, 250);
|
||||
--bg-secondary: rgb(250, 248, 245);
|
||||
--bg-tertiary: rgb(245, 243, 240);
|
||||
--bg-elevated: rgb(255, 255, 255);
|
||||
--border-subtle: rgba(220, 215, 210, 0.5);
|
||||
--border-default: rgba(220, 215, 210, 0.8);
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #5a5a5a;
|
||||
--text-tertiary: #888888;
|
||||
--accent-blue: #7c3aed;
|
||||
--accent-green: #059669;
|
||||
--accent-purple: #7c3aed;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -803,14 +851,17 @@
|
||||
border: 1px solid transparent;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.widget-value-editable:hover {
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.widget-editor-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.widget-editor-input {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
@@ -822,9 +873,11 @@
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.widget-editor-input:focus {
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.widget-editor-select {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
@@ -1086,61 +1139,81 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send button click to main window via BroadcastChannel
|
||||
* This invokes the widget's callback function on the main canvas
|
||||
*/
|
||||
function sendButtonClick(nodeId, widgetName) {
|
||||
channel.postMessage({
|
||||
type: 'button-click',
|
||||
data: {
|
||||
nodeId: nodeId,
|
||||
widgetName: widgetName
|
||||
},
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing a widget value inline
|
||||
*/
|
||||
function editWidgetValue(element, nodeId, widgetName, currentValue, widgetType, options) {
|
||||
// Already editing?
|
||||
if (element.querySelector('.widget-editor-inline')) return;
|
||||
|
||||
|
||||
const originalHtml = element.innerHTML;
|
||||
const isCombo = widgetType === 'combo' && Array.isArray(options) && options.length > 0;
|
||||
// Check for combo by looking at options array, not just type
|
||||
const isCombo = Array.isArray(options) && options.length > 0;
|
||||
const isNumber = widgetType === 'number' || widgetType === 'int' || widgetType === 'float';
|
||||
const isBoolean = widgetType === 'toggle' || widgetType === 'boolean';
|
||||
|
||||
|
||||
let editorHtml;
|
||||
|
||||
|
||||
if (isCombo) {
|
||||
const optionsHtml = options.map(opt =>
|
||||
const optionsHtml = options.map(opt =>
|
||||
`<option value="${escapeHtml(String(opt))}" ${opt == currentValue ? 'selected' : ''}>${escapeHtml(String(opt))}</option>`
|
||||
).join('');
|
||||
editorHtml = `<select class="widget-editor-select" onchange="commitWidgetEdit(this, ${nodeId}, '${widgetName}')">${optionsHtml}</select>`;
|
||||
} else if (isBoolean) {
|
||||
// Toggle immediately for boolean
|
||||
const newValue = currentValue !== true && currentValue !== 'true';
|
||||
// Read the CURRENT displayed value from the element text, not the stale parameter
|
||||
const displayedText = element.textContent?.trim().toLowerCase();
|
||||
const currentBool = displayedText === 'true';
|
||||
const newValue = !currentBool;
|
||||
sendWidgetEdit(nodeId, widgetName, newValue);
|
||||
element.innerHTML = `<span style="color: ${newValue ? 'var(--accent-green)' : 'var(--text-tertiary)'}">${newValue ? 'true' : 'false'}</span>`;
|
||||
// Update display - keep it simple, just update text and color
|
||||
element.textContent = newValue ? 'true' : 'false';
|
||||
element.style.color = newValue ? 'var(--accent-green)' : 'var(--text-tertiary)';
|
||||
return;
|
||||
} else {
|
||||
const inputType = isNumber ? 'number' : 'text';
|
||||
editorHtml = `<input type="${inputType}" class="widget-editor-input" value="${escapeHtml(String(currentValue))}"
|
||||
onblur="commitWidgetEdit(this, ${nodeId}, '${widgetName}')"
|
||||
onkeydown="if(event.key==='Enter'){this.blur();}if(event.key==='Escape'){cancelWidgetEdit(this,'${escapeHtml(originalHtml.replace(/'/g, "\\\\'" ))}');}"/>`;
|
||||
onkeydown="if(event.key==='Enter'){this.blur();}if(event.key==='Escape'){cancelWidgetEdit(this,'${escapeHtml(originalHtml.replace(/'/g, "\\\\'"))}');}"/>`;
|
||||
}
|
||||
|
||||
|
||||
element.innerHTML = `<div class="widget-editor-inline">${editorHtml}</div>`;
|
||||
|
||||
|
||||
const input = element.querySelector('input, select');
|
||||
if (input) {
|
||||
input.focus();
|
||||
if (input.select) input.select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commit widget edit and send to main window
|
||||
*/
|
||||
function commitWidgetEdit(input, nodeId, widgetName) {
|
||||
const value = input.type === 'number' ? parseFloat(input.value) : input.value;
|
||||
sendWidgetEdit(nodeId, widgetName, value);
|
||||
|
||||
|
||||
// Update display
|
||||
const container = input.closest('.widget-value-editable');
|
||||
if (container) {
|
||||
container.innerHTML = `<span style="color: var(--accent-blue);">${escapeHtml(String(value))}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancel widget edit
|
||||
*/
|
||||
@@ -1248,7 +1321,7 @@
|
||||
<!-- Dynamic Lists (Inputs/Outputs/Widgets) -->
|
||||
${renderList('Inputs', info.hoveredNode.inputs)}
|
||||
${renderList('Outputs', info.hoveredNode.outputs)}
|
||||
${renderList('Widgets', info.hoveredNode.widgets)}
|
||||
${renderList('Widgets', info.hoveredNode.widgets, info.hoveredNode.id)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1317,7 +1390,7 @@
|
||||
return String(pos);
|
||||
}
|
||||
|
||||
function renderList(title, items) {
|
||||
function renderList(title, items, nodeId = null) {
|
||||
if (!items || items.length === 0) return '';
|
||||
|
||||
// Icon mapping for different list types
|
||||
@@ -1344,16 +1417,150 @@
|
||||
};
|
||||
const badgeColor = badgeColors[title] || '#888';
|
||||
|
||||
// Check if this is the Widgets section and we have a nodeId for editing
|
||||
const isEditable = title === 'Widgets' && nodeId != null;
|
||||
|
||||
const listHtml = items.map((item, index) => {
|
||||
const name = escapeHtml(item.name || item.label || 'Unknown');
|
||||
const type = item.type ? escapeHtml(item.type) : null;
|
||||
const rawValue = item.value !== undefined ? String(item.value) : null;
|
||||
const itemId = `${title.toLowerCase()}-${index}`;
|
||||
|
||||
// Detect toggle-style "button" widgets (have boolean values)
|
||||
// These are buttons that toggle on/off state
|
||||
const isButtonType = type && type.toLowerCase() === 'button';
|
||||
const hasBooleanValue = rawValue === 'true' || rawValue === 'false';
|
||||
const isToggleButton = isButtonType && hasBooleanValue;
|
||||
|
||||
// True action buttons have null/undefined values or are explicitly action-oriented
|
||||
const isActionButton = isButtonType && !hasBooleanValue &&
|
||||
(rawValue === 'null' || rawValue === 'undefined' || rawValue === null);
|
||||
|
||||
// Check if value contains a data URI image (chart/plot)
|
||||
const isImageValue = rawValue && rawValue.includes("data:image");
|
||||
const value = isImageValue ? rawValue : (rawValue ? escapeHtml(rawValue) : null);
|
||||
|
||||
// For toggle-style button widgets, render a toggle button with on/off visual indication
|
||||
if (isToggleButton && nodeId != null) {
|
||||
const isOn = rawValue === 'true';
|
||||
const toggleColor = isOn ? 'var(--accent-green, #22c55e)' : 'var(--text-tertiary, #666)';
|
||||
const toggleBg = isOn ? 'var(--accent-green, #22c55e)' : 'var(--bg-tertiary, #333)';
|
||||
const toggleText = isOn ? 'ON' : 'OFF';
|
||||
|
||||
return `
|
||||
<div style="
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 8px;">
|
||||
<span style="
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
">${name}</span>
|
||||
<span style="
|
||||
font-size: 8px;
|
||||
padding: 2px 5px;
|
||||
background: ${badgeColor}22;
|
||||
color: ${badgeColor};
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
">TOGGLE</span>
|
||||
</div>
|
||||
<button
|
||||
id="${itemId}"
|
||||
onclick="
|
||||
const currentState = this.dataset.state === 'true';
|
||||
const newState = !currentState;
|
||||
this.dataset.state = String(newState);
|
||||
sendWidgetEdit(${nodeId}, '${escapeHtml(item.name || '')}', newState);
|
||||
this.textContent = newState ? '✓ ON' : '○ OFF';
|
||||
this.style.background = newState ? 'var(--accent-green, #22c55e)' : 'var(--bg-tertiary, #333)';
|
||||
this.style.color = newState ? 'var(--bg-primary, #1a1a1a)' : 'var(--text-secondary, #aaa)';
|
||||
this.style.borderColor = newState ? 'var(--accent-green, #22c55e)' : 'var(--border-default, #555)';
|
||||
this.style.transform='scale(0.95)';
|
||||
setTimeout(() => this.style.transform='', 100);
|
||||
"
|
||||
data-state="${isOn}"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
background: ${toggleBg};
|
||||
color: ${isOn ? 'var(--bg-primary, #1a1a1a)' : 'var(--text-secondary, #aaa)'};
|
||||
border: 2px solid ${isOn ? 'var(--accent-green, #22c55e)' : 'var(--border-default, #555)'};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
"
|
||||
onmouseover="this.style.transform='translateY(-1px)';"
|
||||
onmouseout="this.style.transform='';"
|
||||
>${isOn ? '✓ ON' : '○ OFF'}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// For true action button widgets, render a clickable styled button
|
||||
if (isActionButton && nodeId != null) {
|
||||
return `
|
||||
<div style="
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 8px;">
|
||||
<span style="
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
">${name}</span>
|
||||
<span style="
|
||||
font-size: 8px;
|
||||
padding: 2px 5px;
|
||||
background: ${badgeColor}22;
|
||||
color: ${badgeColor};
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
">ACTION</span>
|
||||
</div>
|
||||
<button
|
||||
onclick="sendButtonClick(${nodeId}, '${escapeHtml(item.name || '')}'); this.style.transform='scale(0.95)'; setTimeout(() => this.style.transform='', 100);"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
background: var(--accent-purple, #a855f7);
|
||||
color: var(--bg-primary, #1a1a1a);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
"
|
||||
onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 3px 8px rgba(0, 0, 0, 0.25)';"
|
||||
onmouseout="this.style.transform=''; this.style.boxShadow='0 1px 3px rgba(0, 0, 0, 0.2)';"
|
||||
>▶ Click</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// For image values, render directly without escaping
|
||||
if (isImageValue) {
|
||||
return `
|
||||
@@ -1390,6 +1597,16 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Get widget options for combo boxes
|
||||
const widgetOptions = item.options ? JSON.stringify(item.options).replace(/"/g, '"') : '[]';
|
||||
|
||||
// Build onclick handler for editable widgets
|
||||
const editableClass = isEditable ? 'widget-value-editable' : '';
|
||||
const editableClick = isEditable && rawValue != null ?
|
||||
`onclick="editWidgetValue(this, ${nodeId}, '${escapeHtml(item.name || '')}', ${JSON.stringify(rawValue).replace(/"/g, '"')}, '${type || 'text'}', ${widgetOptions})"` : '';
|
||||
const editableStyle = isEditable ? 'cursor: pointer;' : '';
|
||||
const editableTitle = isEditable ? 'title="Click to edit"' : '';
|
||||
|
||||
return `
|
||||
<div style="
|
||||
padding: 10px 12px;
|
||||
@@ -1418,42 +1635,27 @@
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
">${type}</span>` : ''}
|
||||
${isEditable ? `<span style="
|
||||
font-size: 9px;
|
||||
color: var(--accent-amber);
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
">✏️ editable</span>` : ''}
|
||||
</div>
|
||||
${value ? `<div style="position: relative;">
|
||||
<div id="${itemId}" style="
|
||||
<div id="${itemId}" class="${editableClass}" ${editableClick} ${editableTitle} style="
|
||||
font-size: 11px;
|
||||
color: var(--accent-blue);
|
||||
font-weight: 500;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 8px 36px 8px 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
line-height: 1.4;
|
||||
${editableStyle}
|
||||
">${value}</div>
|
||||
<button onclick="copyToClipboard('${rawValue.replace(/'/g, "\\'")}', this)" style="
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: all 0.15s ease;
|
||||
" onmouseover="this.style.opacity='1'; this.style.background='var(--bg-tertiary)'"
|
||||
onmouseout="this.style.opacity='0.7'; this.style.background='var(--bg-secondary)'"
|
||||
title="Copy to clipboard">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user