3099 lines
186 KiB
JavaScript
3099 lines
186 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
||
import { api } from "../../scripts/api.js";
|
||
|
||
const comfyApp = app || window.comfyAPI?.app?.app;
|
||
|
||
/*
|
||
* ComfyUI Linear Theme
|
||
* Comprehensive overhaul — ~200 CSS variables, exhaustive selectors.
|
||
* Aesthetic: Ultra-clean monochrome dark, zinc scale, creative tool feel.
|
||
*/
|
||
|
||
// Keep in sync with pyproject.toml version (both are bumped on release)
|
||
const LINEAR_THEME_VERSION = "2.0.7";
|
||
|
||
const THEME = {
|
||
CLEAR_BACKGROUND_COLOR: "#0d0d0d",
|
||
NODE_TITLE_COLOR: "#ffffff",
|
||
NODE_SELECTED_TITLE_COLOR: "#ffffff",
|
||
NODE_TEXT_COLOR: "#ffffff",
|
||
NODE_DEFAULT_COLOR: "#1a1a1a",
|
||
NODE_DEFAULT_BGCOLOR: "#1a1a1a",
|
||
NODE_DEFAULT_BOXCOLOR: "#ffffff",
|
||
NODE_BOX_OUTLINE_COLOR: "#ffffff",
|
||
NODE_BYPASS_BGCOLOR: "#7737AA",
|
||
NODE_ERROR_COLOUR: "#ef4444",
|
||
DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.15)",
|
||
WIDGET_BGCOLOR: "#1a1a1a",
|
||
WIDGET_OUTLINE_COLOR: "#1a1a1a",
|
||
WIDGET_TEXT_COLOR: "#ffffff",
|
||
WIDGET_SECONDARY_TEXT_COLOR: "#b0b0b0",
|
||
LINK_COLOR: "#52525b",
|
||
EVENT_LINK_COLOR: "#a8a29e",
|
||
SLOT_COLORS: {
|
||
CLIP: "#FACC15",
|
||
CLIP_VISION: "#67E8F9",
|
||
CLIP_VISION_OUTPUT: "#ad7452",
|
||
CONDITIONING: "#FB923C",
|
||
CONTROL_NET: "#34D399",
|
||
IMAGE: "#60A5FA",
|
||
LATENT: "#F0ABFC",
|
||
MASK: "#4ADE80",
|
||
MODEL: "#A78BFA",
|
||
STYLE_MODEL: "#C2FFAE",
|
||
VAE: "#F87171",
|
||
TAESD: "#D4A955",
|
||
PIPE_LINE: "#7737AA",
|
||
PIPE_LINE_SDXL: "#7737AA",
|
||
INT: "#29699C",
|
||
X_Y: "#38291f",
|
||
XYPLOT: "#74DA5D",
|
||
LORA_STACK: "#94dccd",
|
||
CONTROL_NET_STACK: "#94dccd",
|
||
FAST_MODEL_LOADER: "#ffd399",
|
||
SAMPLING: "#60A5FA"
|
||
}
|
||
};
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
CSS — Variables + Selectors + Animations
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
|
||
// Injected at module load, decoupled from the extension API: the font-forcing
|
||
// rules (CLAUDE.md Bug #1) must survive even if registerExtension changes or fails.
|
||
function injectLinearThemeCSS() {
|
||
document.getElementById("linear-theme-css")?.remove();
|
||
const style = document.createElement("style");
|
||
style.id = "linear-theme-css";
|
||
style.textContent = CSS;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
const CSS = `
|
||
/* ============================================
|
||
Premium Dark v3 — Complete CSS Overhaul
|
||
Ultra-clean monochrome creative tool theme.
|
||
Zinc scale, alive but minimal.
|
||
============================================ */
|
||
|
||
:root {
|
||
--fg-color: #e4e4e7;
|
||
--bg-color: #0d0d0d;
|
||
--primary-background: rgba(244,244,245,0.1);
|
||
--primary-background-hover: rgba(244,244,245,0.15);
|
||
--secondary-background: rgba(244,244,245,0.08);
|
||
--secondary-background-hover: rgba(244,244,245,0.12);
|
||
--comfy-menu-bg: #1a1a1a;
|
||
--comfy-input-bg: #1a1a1a;
|
||
--input-text: #e4e4e7;
|
||
--descrip-text: #63636e;
|
||
--drag-text: #8e8e99;
|
||
--error-text: #f87171;
|
||
--border-color: #1c1c20;
|
||
--border-default: #242429;
|
||
--content-hover-fg: #f4f4f5;
|
||
--contrast-mix-color: #ffffff;
|
||
--tr-even-bg-color: rgba(18,18,20,0.9);
|
||
--tr-odd-bg-color: rgba(12,12,14,0.9);
|
||
--comfy-menu-secondary-bg: #1a1a1a;
|
||
--linear-accent: #e4e4e7;
|
||
--linear-accent-hover: #f4f4f5;
|
||
--linear-accent-subtle: rgba(244,244,245,0.06);
|
||
--linear-accent-secondary: #a1a1aa;
|
||
--linear-surface: #1a1a1a;
|
||
--linear-surface-elevated: #111113;
|
||
--linear-surface-raised: #1e1e21;
|
||
--linear-border: #1c1c20;
|
||
--linear-border-subtle: #151517;
|
||
--linear-border-hover: #2c2c31;
|
||
--linear-text-primary: #e4e4e7;
|
||
--linear-text-secondary: #a1a1aa;
|
||
--linear-text-muted: #63636e;
|
||
--linear-text-faint: #48484f;
|
||
--linear-danger: #f87171;
|
||
--linear-danger-hover: #ef4444;
|
||
--linear-success: #4ade80;
|
||
--linear-warning: #fbbf24;
|
||
--base-background: #0d0d0d;
|
||
--base-foreground: #e4e4e7;
|
||
--component-node-background: #1a1a1a;
|
||
--component-node-background-hover: rgba(26,26,26,0.92);
|
||
--component-node-border: #1a1a1a;
|
||
--component-node-border-selected: #e4e4e7;
|
||
--component-node-title-color: #ffffff;
|
||
--component-node-text-color: #ffffff;
|
||
--component-node-shadow: rgba(0,0,0,0.15);
|
||
--component-node-widget-background: #1a1a1a;
|
||
--component-node-widget-background-highlighted: #222224;
|
||
--component-node-widget-background-hover: #1e1e21;
|
||
--component-node-widget-border: #1e1e21;
|
||
--component-node-widget-border-hover: #2c2c31;
|
||
--component-node-widget-text: #e4e4e7;
|
||
--component-node-widget-secondary-text: #a1a1aa;
|
||
--interface-menu-surface: rgba(0,0,0,0.84);
|
||
--interface-menu-border: #1c1c20;
|
||
--interface-menu-item-hover: rgba(244,244,245,0.05);
|
||
--interface-menu-item-active: rgba(244,244,245,0.08);
|
||
--interface-menu-text: #e4e4e7;
|
||
--interface-menu-text-secondary: #8e8e99;
|
||
--linear-bars-bg: #1a1a1a;
|
||
--p-highlight-background: rgba(244,244,245,0.07) !important;
|
||
--p-highlight-color: rgba(244,244,245,0.9) !important;
|
||
--p-divider-border-color: #242429;
|
||
--interface-stroke: #1c1c20;
|
||
--interface-panel-surface: #1a1a1a;
|
||
--interface-panel-border: #1c1c20;
|
||
--interface-panel-header: #1a1a1a;
|
||
--interface-panel-selected-surface: rgba(244,244,245,0.07);
|
||
--interface-panel-hover-surface: rgba(244,244,245,0.035);
|
||
--dialog-surface: #1a1a1a;
|
||
--dialog-border: #1c1c20;
|
||
--dialog-overlay: rgba(0,0,0,0.65);
|
||
--modal-card-background: #1a1a1a;
|
||
--modal-card-border: #1c1c20;
|
||
--button-surface: #141416;
|
||
--button-surface-hover: #1e1e21;
|
||
--button-active-surface: #222224;
|
||
--button-hover-surface: #1e1e21;
|
||
--button-border: #222224;
|
||
--button-border-hover: #2c2c31;
|
||
--button-text: #e4e4e7;
|
||
--button-text-secondary: #8e8e99;
|
||
--input-surface: #131315;
|
||
--input-border: #1c1c20;
|
||
--input-border-focus: #a1a1aa;
|
||
--input-placeholder: #48484f;
|
||
--node-component-border: #1e1e21;
|
||
--node-component-surface: #111113;
|
||
--node-component-surface-hover: #1e1e21;
|
||
--text-primary: #e4e4e7;
|
||
--text-secondary: #8e8e99;
|
||
--text-muted: #63636e;
|
||
--text-faint: #48484f;
|
||
--color-text-primary: #e4e4e7;
|
||
--color-text-secondary: #8e8e99;
|
||
--p-surface-0: #0d0d0d;
|
||
--p-surface-50: #0d0d0d;
|
||
--p-surface-100: #1a1a1a;
|
||
--p-surface-200: #1a1a1a;
|
||
--p-surface-300: #1e1e21;
|
||
--p-surface-400: #1c1c20;
|
||
--p-surface-500: #242429;
|
||
--p-surface-600: #3a3a41;
|
||
--p-surface-700: #48484f;
|
||
--p-surface-800: #63636e;
|
||
--p-surface-900: #8e8e99;
|
||
--p-primary-color: #e4e4e7;
|
||
--p-primary-color-hover: #f4f4f5;
|
||
--p-primary-color-focus: rgba(228,228,231,0.2);
|
||
--p-primary-contrast-color: #09090b;
|
||
--p-text-color: #e4e4e7;
|
||
--p-text-muted-color: #63636e;
|
||
--p-text-secondary-color: #a1a1aa;
|
||
--p-content-background: #1a1a1a;
|
||
--p-content-border-color: #1c1c20;
|
||
--p-content-color: #e4e4e7;
|
||
--p-content-hover-background: rgba(244,244,245,0.05);
|
||
--p-content-hover-color: #f4f4f5;
|
||
--p-overlay-background: #1a1a1a;
|
||
--p-overlay-border-color: #1c1c20;
|
||
--p-mask-background: rgba(0,0,0,0.65);
|
||
--p-form-field-background: #131315;
|
||
--p-form-field-border-color: #1c1c20;
|
||
--p-form-field-focus-border-color: #a1a1aa;
|
||
--p-form-field-color: #e4e4e7;
|
||
--p-form-field-placeholder-color: #48484f;
|
||
--p-form-field-hover-border-color: #2c2c31;
|
||
--p-form-field-disabled-background: #1a1a1a;
|
||
--p-form-field-disabled-color: #3a3a41;
|
||
--p-navigation-item-active-background: rgba(244,244,245,0.07);
|
||
--p-navigation-item-focus-background: rgba(244,244,245,0.05);
|
||
--p-navigation-item-color: #8e8e99;
|
||
--p-navigation-item-focus-color: #e4e4e7;
|
||
--p-navigation-item-active-color: #f4f4f5;
|
||
--p-anchor-gutter: 2px;
|
||
--p-focus-ring-color: rgba(161,161,170,0.35);
|
||
--p-focus-ring-width: 2px;
|
||
--p-focus-ring-style: solid;
|
||
--p-focus-ring-offset: 2px;
|
||
--p-tabs-tablist-background: transparent;
|
||
--p-tabs-tablist-border-color: #1c1c20;
|
||
--p-tabs-tab-background: transparent;
|
||
--p-tabs-tab-color: #63636e;
|
||
--p-tabs-tab-hover-color: #e4e4e7;
|
||
--p-tabs-tab-active-color: #f4f4f5;
|
||
--p-tabs-tab-active-border-color: #e4e4e7;
|
||
--p-tabs-tabpanel-background: transparent;
|
||
--p-tabs-active-bar-background: #e4e4e7;
|
||
--p-dialog-background: #1a1a1a;
|
||
--p-dialog-border-color: #1c1c20;
|
||
--p-dialog-color: #e4e4e7;
|
||
--p-dialog-title-font-weight: 600;
|
||
--p-button-background: #141416;
|
||
--p-button-border-color: #1c1c20;
|
||
--p-button-color: #8e8e99;
|
||
--p-button-hover-background: #1e1e21;
|
||
--p-button-hover-border-color: #2c2c31;
|
||
--p-button-hover-color: #e4e4e7;
|
||
--p-button-active-background: #222224;
|
||
--p-button-active-border-color: #2c2c31;
|
||
--p-button-focus-ring-color: rgba(161,161,170,0.35);
|
||
--p-button-primary-background: #141416;
|
||
--p-button-primary-border-color: #1c1c20;
|
||
--p-button-primary-color: #8e8e99;
|
||
--p-button-primary-hover-background: #1e1e21;
|
||
--p-button-primary-hover-border-color: #2c2c31;
|
||
--p-button-primary-hover-color: #e4e4e7;
|
||
--p-button-primary-active-background: #222224;
|
||
--p-button-primary-active-border-color: #2c2c31;
|
||
--p-select-background: #131315;
|
||
--p-select-border-color: #1c1c20;
|
||
--p-select-color: #e4e4e7;
|
||
--p-select-hover-border-color: #2c2c31;
|
||
--p-select-focus-border-color: #a1a1aa;
|
||
--p-select-overlay-background: #0e0e10;
|
||
--p-select-overlay-border-color: #1c1c20;
|
||
--p-select-option-color: #e4e4e7;
|
||
--p-select-option-focus-background: rgba(244,244,245,0.05);
|
||
--p-select-option-focus-color: #f4f4f5;
|
||
--p-select-option-selected-background: rgba(244,244,245,0.08);
|
||
--p-select-option-selected-color: #f4f4f5;
|
||
--p-select-option-selected-focus-background: rgba(244,244,245,0.1);
|
||
--p-select-option-selected-focus-color: #f4f4f5;
|
||
--p-select-placeholder-color: #48484f;
|
||
--p-inputtext-background: #131315;
|
||
--p-inputtext-border-color: #1c1c20;
|
||
--p-inputtext-color: #e4e4e7;
|
||
--p-inputtext-hover-border-color: #2c2c31;
|
||
--p-inputtext-focus-border-color: #a1a1aa;
|
||
--p-inputtext-placeholder-color: #48484f;
|
||
--p-checkbox-background: #131315;
|
||
--p-checkbox-border-color: #1c1c20;
|
||
--p-checkbox-hover-border-color: #2c2c31;
|
||
--p-checkbox-checked-background: #e4e4e7;
|
||
--p-checkbox-checked-border-color: #e4e4e7;
|
||
--p-checkbox-checked-color: #09090b;
|
||
--p-checkbox-icon-checked-color: #09090b;
|
||
--p-toggleswitch-background: #222224;
|
||
--p-toggleswitch-border-color: #2c2c31;
|
||
--p-toggleswitch-checked-background: #e4e4e7;
|
||
--p-toggleswitch-checked-border-color: #e4e4e7;
|
||
--p-toggleswitch-handle-background: #63636e;
|
||
--p-toggleswitch-handle-checked-background: #09090b;
|
||
--p-datatable-header-background: #1a1a1a;
|
||
--p-datatable-header-border-color: #1c1c20;
|
||
--p-datatable-header-color: #63636e;
|
||
--p-datatable-body-background: transparent;
|
||
--p-datatable-row-background: transparent;
|
||
--p-datatable-row-color: #e4e4e7;
|
||
--p-datatable-row-hover-background: rgba(244,244,245,0.035);
|
||
--p-datatable-row-hover-color: #f4f4f5;
|
||
--p-datatable-body-cell-border-color: #151517;
|
||
--p-toast-background: #111113;
|
||
--p-toast-border-color: #1c1c20;
|
||
--p-toast-color: #e4e4e7;
|
||
--p-tooltip-background: #131315;
|
||
--p-tooltip-border-color: #1c1c20;
|
||
--p-tooltip-color: #e4e4e7;
|
||
--p-progressbar-background: #131315;
|
||
--p-progressbar-value-background: #e4e4e7;
|
||
--p-tag-background: rgba(244,244,245,0.06);
|
||
--p-tag-color: #e4e4e7;
|
||
--p-badge-background: #e4e4e7;
|
||
--p-badge-color: #09090b;
|
||
--p-menu-background: #0e0e10;
|
||
--p-menu-border-color: #1c1c20;
|
||
--p-menu-color: #e4e4e7;
|
||
--p-menu-item-color: #e4e4e7;
|
||
--p-menu-item-focus-background: rgba(244,244,245,0.05);
|
||
--p-menu-item-focus-color: #f4f4f5;
|
||
--p-drawer-background: #1a1a1a;
|
||
--p-drawer-border-color: #1c1c20;
|
||
--p-sidebar-background: #1a1a1a;
|
||
--p-sidebar-border-color: #1c1c20;
|
||
--p-tree-background: transparent;
|
||
--p-tree-color: #e4e4e7;
|
||
--p-tree-node-hover-background: rgba(244,244,245,0.035);
|
||
--p-tree-node-selected-background: rgba(244,244,245,0.07);
|
||
--p-tree-node-color: #e4e4e7;
|
||
--p-tree-node-hover-color: #f4f4f5;
|
||
--p-tree-node-selected-color: #f4f4f5;
|
||
--p-scrollbar-track-background: transparent;
|
||
--p-scrollbar-thumb-background: #242429;
|
||
--p-scrollbar-thumb-hover-background: #3a3a41;
|
||
--p-divider-color: #1c1c20;
|
||
--p-skeleton-background: #131315;
|
||
--p-skeleton-animation-background: #1e1e21;
|
||
}
|
||
|
||
/* ── Base body ── */
|
||
body, body.litegraph { background: var(--bg-color, #0d0d0d) !important; color: var(--fg-color, #e4e4e7) !important; font-family: 'Inter', Arial, sans-serif !important; }
|
||
|
||
/* ── Dialog / Modal ── */
|
||
.manager-dialog, .p-dialog { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 14px !important; box-shadow: none !important; color: var(--linear-text-primary) !important; overflow: hidden !important; }
|
||
.p-dialog-mask { background: var(--dialog-overlay) !important; }
|
||
.p-dialog-header { background: transparent !important; border-bottom: 1px solid var(--linear-border) !important; color: var(--linear-text-primary) !important; padding: 18px 24px !important; font-size: 15px !important; letter-spacing: -0.01em !important; }
|
||
.p-dialog-content { background: var(--linear-surface) !important; color: var(--linear-text-primary) !important; padding: 20px 24px !important; }
|
||
.p-dialog-footer { background: var(--linear-surface-elevated) !important; border-top: 1px solid var(--linear-border) !important; padding: 14px 24px !important; }
|
||
|
||
/* ── Buttons ── */
|
||
.p-button, button.cm-button, .dialog button, .comfy-modal button, .manager-dialog button, .comfyui-button, .comfyui-button button, .comfyui-split-button button { background: var(--button-surface) !important; border: 1px solid var(--button-border) !important; border-radius: 7px !important; color: var(--linear-text-secondary) !important; font-weight: 500 !important; font-size: 13px !important; letter-spacing: -0.005em !important; box-shadow: none !important; cursor: pointer !important; }
|
||
.p-button:hover, button.cm-button:hover, .dialog button:hover, .comfy-modal button:hover, .manager-dialog button:hover, .comfyui-button:hover, .comfyui-button button:hover, .comfyui-split-button button:hover { background: var(--button-surface-hover) !important; border-color: var(--button-border-hover) !important; color: var(--linear-text-primary) !important; }
|
||
.p-button-primary { background: var(--button-surface) !important; border: 1px solid var(--button-border) !important; color: var(--linear-text-secondary) !important; }
|
||
.p-button-primary:hover { background: var(--button-surface-hover) !important; border-color: var(--button-border-hover) !important; color: var(--linear-text-primary) !important; }
|
||
.p-button-danger, button[style*="background-color: red"], button[style*="background: red"] { background: rgba(248,113,113,0.12) !important; border-color: rgba(248,113,113,0.25) !important; color: #f87171 !important; }
|
||
.p-button-danger:hover, button[style*="background-color: red"]:hover, button[style*="background: red"]:hover { background: rgba(248,113,113,0.18) !important; border-color: rgba(248,113,113,0.4) !important; color: #fca5a5 !important; }
|
||
.p-button-text, .p-button-outlined { background: transparent !important; border-color: transparent !important; color: var(--linear-text-secondary) !important; }
|
||
.p-button-text:hover, .p-button-outlined:hover { background: var(--linear-accent-subtle) !important; color: var(--linear-text-primary) !important; }
|
||
.p-button-secondary { background: var(--button-surface) !important; border-color: var(--button-border) !important; color: var(--linear-text-secondary) !important; }
|
||
.p-button-secondary:hover { background: var(--button-surface-hover) !important; border-color: var(--button-border-hover) !important; color: var(--linear-text-primary) !important; }
|
||
|
||
/* ── Selects / Dropdowns ── */
|
||
select, .p-select, .p-dropdown { background: var(--input-surface) !important; border: 1px solid var(--input-border) !important; border-radius: 7px !important; color: var(--linear-text-primary) !important; font-size: 13px !important; box-shadow: none !important; }
|
||
select:hover, .p-select:hover, .p-dropdown:hover { border-color: var(--linear-border-hover) !important; }
|
||
select:focus, .p-select:focus, .p-select.p-focus, .p-dropdown:focus { border-color: var(--linear-accent-secondary) !important; outline: none !important; }
|
||
select option, select optgroup { background: #0e0e10 !important; color: #e4e4e7 !important; }
|
||
.p-select-overlay, .p-dropdown-panel, .p-listbox, .p-popover { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; overflow: hidden !important; }
|
||
.p-select-option, .p-dropdown-item, .p-listbox-option { color: var(--linear-text-primary) !important; border-radius: 5px !important; margin: 2px 4px !important; padding: 7px 10px !important; }
|
||
.p-select-option:hover, .p-dropdown-item:hover, .p-listbox-option:hover, .p-select-option.p-focus, .p-listbox-option.p-focus { background: rgba(244,244,245,0.05) !important; }
|
||
.p-select-option.p-selected, .p-select-option.p-highlight, .p-dropdown-item.p-highlight, .p-listbox-option.p-selected, .p-listbox-option.p-highlight { background: rgba(244,244,245,0.08) !important; color: #f4f4f5 !important; }
|
||
|
||
/* ── Inputs ── */
|
||
input[type="text"], input[type="number"], input[type="search"], input[type="password"], input[type="email"], input[type="url"], textarea, .p-inputtext, .p-textarea, .comfy-multiline-input { background: var(--input-surface) !important; border: 1px solid var(--input-border) !important; border-radius: 7px !important; color: var(--linear-text-primary) !important; font-family: 'Inter', Arial, sans-serif !important; font-size: 13px !important; box-shadow: none !important; }
|
||
textarea.comfy-multiline-input { background: #1a1a1a !important; border-color: #1a1a1a !important; outline: none !important; }
|
||
input:hover, textarea:hover, .p-inputtext:hover { border-color: var(--linear-border-hover) !important; }
|
||
input:focus, textarea:focus, .p-inputtext:focus, .p-inputtext.p-focus { border-color: var(--linear-accent-secondary) !important; outline: none !important; }
|
||
input::placeholder, textarea::placeholder { color: var(--input-placeholder) !important; }
|
||
|
||
/* ── Tabs ── */
|
||
.p-tablist, .p-tabs { background: transparent !important; border-bottom: 1px solid var(--linear-border) !important; }
|
||
.p-tab, .p-tabpanel { color: var(--linear-text-muted) !important; background: transparent !important; border: none !important; font-size: 13px !important; font-weight: 500 !important; }
|
||
.p-tab:hover { color: var(--linear-text-primary) !important; }
|
||
.p-tab-active, .p-tab[data-p-active="true"], .p-tab.p-tab-active { color: #f4f4f5 !important; border-bottom: 2px solid var(--linear-accent) !important; }
|
||
.p-tablist .p-tabs-active-bar, .p-tab-active-bar { background: var(--linear-accent) !important; }
|
||
.workflow-tab, .workflow-tabs { background: transparent !important; border-color: var(--linear-border) !important; }
|
||
.workflow-tab { color: var(--linear-text-muted) !important; font-size: 12px !important; font-weight: 500 !important; }
|
||
.workflow-tab:hover { color: var(--linear-text-primary) !important; background: var(--linear-accent-subtle) !important; }
|
||
.workflow-tab.active, .workflow-tab[data-active="true"] { color: #f4f4f5 !important; background: rgba(244,244,245,0.06) !important; }
|
||
|
||
/* ── Tables ── */
|
||
.p-datatable, table { background: transparent !important; }
|
||
.p-datatable-thead > tr > th, table th { background: var(--linear-surface) !important; border-bottom: 1px solid var(--linear-border) !important; color: var(--linear-text-muted) !important; font-weight: 500 !important; font-size: 11px !important; text-transform: uppercase !important; letter-spacing: 0.04em !important; padding: 10px 14px !important; }
|
||
.p-datatable-tbody > tr, table tr { border-bottom: 1px solid var(--linear-border-subtle) !important; }
|
||
.p-datatable-tbody > tr:hover, table tr:hover { background: rgba(244,244,245,0.025) !important; }
|
||
.p-datatable-tbody > tr > td, table td { color: var(--linear-text-primary) !important; padding: 10px 14px !important; font-size: 13px !important; }
|
||
|
||
/* ── Scrollbars ── */
|
||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: #222224; border-radius: 3px; }
|
||
::-webkit-scrollbar-thumb:hover { background: #3a3a41; }
|
||
* { scrollbar-width: thin; scrollbar-color: #222224 transparent; }
|
||
|
||
/* ── Tooltips ── */
|
||
.p-tooltip { background: var(--linear-surface-elevated) !important; border: 1px solid var(--linear-border) !important; border-radius: 7px !important; color: var(--linear-text-primary) !important; font-size: 12px !important; }
|
||
.p-tooltip .p-tooltip-text { background: transparent !important; color: inherit !important; }
|
||
|
||
/* ── Checkboxes / Toggles ── */
|
||
.p-checkbox .p-checkbox-box, .p-checkbox-box { background: var(--input-surface) !important; border: 1px solid #242429 !important; border-radius: 4px !important; }
|
||
.p-checkbox .p-checkbox-box.p-highlight, .p-checkbox-box.p-highlight, .p-checkbox.p-checkbox-checked .p-checkbox-box { background: #e4e4e7 !important; border-color: #e4e4e7 !important; }
|
||
.p-checkbox .p-checkbox-icon, .p-checkbox-icon { color: #09090b !important; }
|
||
.p-toggleswitch .p-toggleswitch-slider, .p-toggleswitch-slider { background: #222224 !important; border: 1px solid #2c2c31 !important; border-radius: 12px !important; }
|
||
.p-toggleswitch.p-toggleswitch-checked .p-toggleswitch-slider, .p-toggleswitch-checked .p-toggleswitch-slider { background: #e4e4e7 !important; border-color: #e4e4e7 !important; }
|
||
|
||
/* ── Badges / Tags ── */
|
||
.p-badge, .p-tag { border-radius: 5px !important; font-weight: 500 !important; font-size: 11px !important; }
|
||
.p-badge { background: #e4e4e7 !important; color: #09090b !important; }
|
||
.p-tag { background: rgba(244,244,245,0.06) !important; color: #a1a1aa !important; border: 1px solid rgba(244,244,245,0.08) !important; }
|
||
|
||
/* ── Menus & Context Menus ── */
|
||
.p-menu, .p-contextmenu, .p-menubar { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; box-shadow: none !important; color: var(--linear-text-primary) !important; overflow: hidden !important; padding: 4px !important; }
|
||
.p-menuitem > .p-menuitem-content, .p-menuitem-link { color: var(--linear-text-primary) !important; border-radius: 6px !important; }
|
||
.p-menuitem:hover > .p-menuitem-content, .p-menuitem-link:hover { background: rgba(244,244,245,0.05) !important; }
|
||
.p-menuitem .p-menuitem-icon { color: var(--linear-text-muted) !important; }
|
||
|
||
/* Litegraph context menu */
|
||
.litegraph.litecontextmenu, .litegraph.litecontextmenu.dark { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; color: var(--linear-text-primary) !important; font-family: 'Inter', Arial, sans-serif !important; overflow: hidden !important; z-index: 9999 !important; padding: 4px !important; }
|
||
.litegraph .litemenu-entry, .litemenu-title { font-family: 'Inter', Arial, sans-serif !important; color: var(--linear-text-secondary) !important; padding: 5px 10px !important; border-radius: 5px !important; margin: 1px 0 !important; }
|
||
.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) { background: rgba(244,244,245,0.05) !important; color: #f4f4f5 !important; }
|
||
.litegraph .litemenu-entry.separator { border-top: 1px solid var(--linear-border) !important; height: 0px !important; margin: 4px 8px !important; padding: 0 !important; background: transparent !important; }
|
||
.litegraph .litemenu-entry.has_submenu { border-right: 2px solid #48484f !important; }
|
||
.litegraph .litemenu-entry.disabled { opacity: 0.4 !important; color: var(--linear-text-muted) !important; }
|
||
.litemenu-title { color: #f4f4f5 !important; background: transparent !important; font-weight: 600 !important; font-size: 12px !important; padding: 8px 10px 6px !important; }
|
||
|
||
/* ── Sidebar / Panels ── */
|
||
.shadow-interface { box-shadow: none !important; }
|
||
[class*="shadow-"] { box-shadow: none !important; }
|
||
.drop-shadow-\(--interface-panel-drop-shadow\) { filter: none !important; --tw-drop-shadow: none !important; }
|
||
.side-tool-bar-container { background: transparent !important; border: none !important; }
|
||
.side-tool-bar-container.connected-sidebar { background-color: transparent !important; border: none !important; }
|
||
.side-tool-bar-container .sidebar-item-group { background-color: #131313 !important; border: none !important; box-shadow: none !important; }
|
||
.p-drawer, .p-sidebar, .comfy-sidebar, .side-bar-panel { background: #1a1a1a !important; border: none !important; box-shadow: none !important; color: var(--linear-text-primary) !important; }
|
||
.side-bar-panel { border: none !important; }
|
||
.p-panel { box-shadow: none !important; }
|
||
.p-panel .p-panel-content { background: #1a1a1a !important; border-color: var(--linear-border) !important; }
|
||
.actionbar-container { position: relative !important; background: #131313 !important; border: none !important; border-radius: 10px !important; isolation: isolate !important; }
|
||
.actionbar-container::before { content: ''; position: absolute; inset: 0; background: #131313; border-radius: inherit; z-index: -1; pointer-events: none; }
|
||
.actionbar.fixed, .actionbar[style*="position: fixed"], .actionbar[style*="position:fixed"] { background: #131313 !important; border: none !important; border-radius: 10px !important; }
|
||
.graph-canvas-panel .p-buttongroup, .graph-canvas-panel span.p-buttongroup, .graph-canvas-panel .p-buttongroup.bg-comfy-menu-bg { background: #131313 !important; background-color: #131313 !important; border: none !important; border-radius: 10px !important; outline: none !important; }
|
||
.graph-canvas-panel .p-buttongroup button,
|
||
.graph-canvas-panel .p-buttongroup .p-button,
|
||
.graph-canvas-panel .p-buttongroup [class*="button"] { background: transparent !important; border: none !important; border-color: transparent !important; outline: none !important; color: #e4e4e7 !important; }
|
||
.graph-canvas-panel .p-buttongroup button:hover,
|
||
.graph-canvas-panel .p-buttongroup .p-button:hover { color: #e4e4e7 !important; background: rgba(244,244,245,0.06) !important; }
|
||
.graph-canvas-panel .p-buttongroup button[aria-pressed="true"],
|
||
.graph-canvas-panel .p-buttongroup .bg-interface-panel-selected-surface { background: rgba(244,244,245,0.08) !important; color: #e4e4e7 !important; border: none !important; }
|
||
.graph-canvas-panel .p-buttongroup [class*="separator"],
|
||
.graph-canvas-panel .p-buttongroup [class*="divider"] { background: #1e1e22 !important; }
|
||
.bottom-panel { background: #1a1a1a !important; border-top: 1px solid var(--linear-border) !important; }
|
||
.side-bar-panel, .bottom-panel, .p-drawer, .p-sidebar { contain: layout style paint !important; }
|
||
.actionbar-container { contain: none !important; }
|
||
/* Scoped to the graph canvas ONLY. A global \`canvas\` rule forced will-change
|
||
on the mask editor's offscreen-rendered overlay canvas, which composites as
|
||
solid black in Chromium (mask editor showed no image — GitHub issue #2). */
|
||
#graph-canvas, .graph-canvas-container > canvas { will-change: transform !important; }
|
||
.graph-canvas-container * { -webkit-font-smoothing: antialiased !important; }
|
||
|
||
/* Graph button / secondary backgrounds */
|
||
.bg-secondary-background { background: #131313 !important; border: none !important; }
|
||
|
||
/* Sidebar icon rail */
|
||
.side-tool-bar-container button { color: #48484f !important; border-radius: 7px !important; background: transparent !important; border: none !important; outline: none !important; }
|
||
.side-tool-bar-container button:hover { color: #a1a1aa !important; background: rgba(244,244,245,0.06) !important; border: none !important; }
|
||
.side-tool-bar-container button.active, .side-tool-bar-container button[aria-selected="true"] { color: #e4e4e7 !important; background: rgba(244,244,245,0.08) !important; border: none !important; }
|
||
hr, .p-divider { border-color: #242429 !important; }
|
||
|
||
/* ── Progress bars ── */
|
||
.p-progressbar { background: var(--linear-surface-elevated) !important; border-radius: 4px !important; height: 3px !important; overflow: hidden !important; }
|
||
.p-progressbar-value { background: #e4e4e7 !important; border-radius: 4px !important; }
|
||
|
||
/* ── Close buttons ── */
|
||
.p-dialog-header-close, .p-button-icon-only { color: var(--linear-text-muted) !important; background: transparent !important; border: none !important; }
|
||
.p-dialog-header-close:hover, .p-button-icon-only:hover { color: var(--linear-text-primary) !important; background: rgba(244,244,245,0.06) !important; border-radius: 6px !important; }
|
||
|
||
/* ── Toast ── */
|
||
.p-toast-message { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; color: var(--linear-text-primary) !important; }
|
||
.p-toast-message-content { color: inherit !important; }
|
||
.p-toast-close-button { color: var(--linear-text-muted) !important; }
|
||
.p-toast-close-button:hover { color: var(--linear-text-primary) !important; background: rgba(244,244,245,0.06) !important; }
|
||
|
||
/* ── Search / Filter ── */
|
||
.p-iconfield, .p-inputicon { color: var(--linear-text-muted) !important; }
|
||
.litesearchbox { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 12px !important; }
|
||
.litesearchbox .helper { overflow: auto !important; max-height: 300px !important; }
|
||
.invisible-dialog-root, .p-dialog.invisible-dialog-root { background: transparent !important; border: none !important; box-shadow: none !important; overflow: visible !important; }
|
||
.node-search-box-dialog-mask, .p-dialog-mask.node-search-box-dialog-mask { background: rgba(0,0,0,0.3) !important; }
|
||
.comfy-vue-node-search-container, .node-search-box { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 12px !important; color: var(--linear-text-primary) !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete, .node-search-box .p-autocomplete { background: transparent !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-input, .node-search-box .p-autocomplete-input, .comfy-vue-node-search-container input, .node-search-box input { background: transparent !important; border: none !important; border-bottom: 1px solid var(--linear-border) !important; border-radius: 0 !important; color: var(--linear-text-primary) !important; font-size: 14px !important; padding: 12px 16px !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-input:focus, .node-search-box .p-autocomplete-input:focus { border-bottom-color: var(--linear-accent-secondary) !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-overlay, .comfy-vue-node-search-container .p-autocomplete-panel, .node-search-box .p-autocomplete-overlay, .node-search-box .p-autocomplete-panel, .p-autocomplete-overlay { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; color: var(--linear-text-primary) !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-option, .comfy-vue-node-search-container .p-autocomplete-item, .node-search-box .p-autocomplete-option, .node-search-box .p-autocomplete-item, .p-autocomplete-option { background: transparent !important; color: var(--linear-text-primary) !important; border-bottom: 1px solid var(--linear-border-subtle) !important; padding: 8px 16px !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-option:hover, .comfy-vue-node-search-container .p-autocomplete-item:hover, .p-autocomplete-option:hover, .p-autocomplete-option.p-focus { background: rgba(244,244,245,0.04) !important; color: #f4f4f5 !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-option.p-highlight, .p-autocomplete-option.p-highlight { background: rgba(244,244,245,0.07) !important; color: #f4f4f5 !important; }
|
||
.comfy-vue-node-search-container .p-tag, .comfy-vue-node-search-container .p-badge, .comfy-vue-node-search-container [class*="badge"], .node-search-box .p-tag { background: rgba(244,244,245,0.05) !important; color: var(--linear-text-muted) !important; border: 1px solid rgba(244,244,245,0.06) !important; border-radius: 5px !important; padding: 2px 8px !important; font-size: 11px !important; font-weight: 400 !important; line-height: 1.4 !important; }
|
||
.comfy-vue-node-search-container .option-badges { display: flex !important; align-items: center !important; gap: 6px !important; }
|
||
.comfy-vue-node-search-container .node-search-item-category, .comfy-vue-node-search-container [class*="category"], .comfy-vue-node-search-container [class*="subtitle"], .comfy-vue-node-search-container [class*="secondary"] { color: var(--linear-text-muted) !important; }
|
||
.comfy-vue-node-search-container .p-button, .node-search-box .p-button { background: transparent !important; border: 1px solid var(--linear-border) !important; color: var(--linear-text-secondary) !important; }
|
||
.comfy-vue-node-search-container .p-autocomplete-empty-message { color: var(--linear-text-muted) !important; padding: 16px !important; text-align: center !important; }
|
||
.litesearchbox input { background: transparent !important; border: none !important; border-bottom: 1px solid var(--linear-border) !important; border-radius: 0 !important; color: var(--linear-text-primary) !important; padding: 12px 16px !important; font-size: 14px !important; }
|
||
.lite-search-item, .litesearchbox .lite-search-item { color: var(--linear-text-secondary) !important; padding: 7px 16px !important; border-radius: 5px !important; margin: 1px 4px !important; }
|
||
.lite-search-item:hover, .litesearchbox .lite-search-item:hover { background: rgba(244,244,245,0.05) !important; color: #f4f4f5 !important; }
|
||
.lite-search-item.selected { background: rgba(244,244,245,0.08) !important; color: #f4f4f5 !important; }
|
||
|
||
/* ── Chip / Tag ── */
|
||
.p-chip { background: rgba(244,244,245,0.06) !important; color: var(--linear-text-primary) !important; border-radius: 5px !important; border: 1px solid rgba(244,244,245,0.08) !important; }
|
||
|
||
/* ── Settings dialog ── */
|
||
.comfy-settings, .settings-container { background: var(--linear-surface) !important; color: var(--linear-text-primary) !important; }
|
||
.comfy-settings .settings-header, .settings-container .settings-header { border-bottom: 1px solid var(--linear-border) !important; }
|
||
.p-dialog:has(.settings-container), .p-dialog:has([class*="settings"]) { border-radius: 14px !important; overflow: hidden !important; }
|
||
.settings-container { background: #0d0d0d !important; color: #e4e4e7 !important; }
|
||
.settings-container .p-splitter-panel:first-child, .settings-container [class*="sidebar"], .settings-container [class*="nav-panel"], .settings-container .left-panel { background: #111113 !important; border-right: 1px solid #1c1c20 !important; }
|
||
.settings-container [class*="section-header"], .settings-container [class*="nav-header"], .settings-container .p-listbox-option-group, .settings-container [class*="uppercase"] { color: #48484f !important; font-size: 10px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; padding: 12px 16px 6px !important; }
|
||
.settings-container .p-listbox-option, .settings-container [class*="nav-item"], .settings-container [class*="setting-category"] { color: #8e8e99 !important; border-radius: 6px !important; margin: 1px 8px !important; padding: 8px 12px !important; font-size: 13px !important; }
|
||
.settings-container .p-listbox-option:hover, .settings-container [class*="nav-item"]:hover { background: rgba(244,244,245,0.04) !important; color: #e4e4e7 !important; }
|
||
.settings-container .p-listbox-option.p-highlight, .settings-container .p-listbox-option.p-selected, .settings-container [class*="nav-item"].active, .settings-container [class*="nav-item"][aria-selected="true"] { background: rgba(244,244,245,0.07) !important; color: #f4f4f5 !important; font-weight: 500 !important; }
|
||
.settings-container .p-splitter-panel:last-child, .settings-container .right-panel, .settings-container [class*="content-panel"] { background: #0d0d0d !important; padding: 24px 32px !important; }
|
||
.settings-container [class*="content-header"], .settings-container h2, .settings-container h3 { color: #e4e4e7 !important; font-weight: 600 !important; letter-spacing: -0.01em !important; }
|
||
.settings-container input[type="search"], .settings-container input[type="text"], .settings-container .p-inputtext, .settings-container [class*="search"] input { background: #131315 !important; border: 1px solid #1c1c20 !important; border-radius: 7px !important; color: #e4e4e7 !important; font-size: 13px !important; height: 36px !important; }
|
||
.settings-container input:focus, .settings-container .p-inputtext:focus { border-color: #a1a1aa !important; outline: none !important; }
|
||
.settings-container [class*="setting-item"], .settings-container [class*="setting-row"] { padding: 16px 0 !important; border-bottom: 1px solid rgba(28,28,32,0.5) !important; }
|
||
.settings-container [class*="setting-label"], .settings-container label { color: #e4e4e7 !important; font-size: 13px !important; font-weight: 500 !important; }
|
||
.settings-container [class*="setting-description"], .settings-container [class*="setting-tooltip"], .settings-container .text-muted { color: #48484f !important; font-size: 12px !important; line-height: 1.5 !important; }
|
||
.settings-container .p-select, .settings-container select { background: #131315 !important; border: 1px solid #1c1c20 !important; border-radius: 7px !important; color: #e4e4e7 !important; font-size: 13px !important; min-height: 36px !important; }
|
||
.settings-container .p-select:hover, .settings-container select:hover { border-color: #2c2c31 !important; }
|
||
.settings-container .p-splitter-gutter { background: transparent !important; width: 1px !important; }
|
||
.settings-container .p-splitter-gutter-handle { background: #1c1c20 !important; }
|
||
|
||
/* ── Lists ── */
|
||
.comfy-list, .comfy-list-items { background: transparent !important; color: var(--linear-text-primary) !important; }
|
||
.comfy-list-items > * { border-bottom: 1px solid var(--linear-border-subtle) !important; padding: 8px 12px !important; }
|
||
.comfy-list-items > *:hover { background: rgba(244,244,245,0.03) !important; }
|
||
.comfyui-popup { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; color: var(--linear-text-primary) !important; }
|
||
.node-title-editor { background: #111113 !important; border-radius: 6px !important; min-width: 300px !important; box-sizing: border-box !important; }
|
||
.editable-text { color: var(--linear-text-primary) !important; }
|
||
.editable-text input, .editable-text textarea { background: var(--input-surface) !important; border: 1px solid var(--linear-accent-secondary) !important; border-radius: 5px !important; color: var(--linear-text-primary) !important; }
|
||
|
||
/* ── Tree / File explorer ── */
|
||
.tree-node, .tree-folder, .p-tree-node, .p-tree-node-content { color: var(--linear-text-primary) !important; border-radius: 6px !important; }
|
||
.tree-node:hover, .tree-folder:hover, .p-tree-node-content:hover { background: rgba(244,244,245,0.035) !important; }
|
||
.tree-node.selected, .p-tree-node-content.p-highlight, .p-tree-node.p-tree-node-selected > .p-tree-node-content { background: rgba(244,244,245,0.07) !important; color: #f4f4f5 !important; }
|
||
|
||
/* ── Markdown ── */
|
||
.comfy-markdown { color: var(--linear-text-primary) !important; }
|
||
.comfy-markdown a { color: #a1a1aa !important; text-decoration: underline !important; text-underline-offset: 2px !important; }
|
||
.comfy-markdown a:hover { color: #f4f4f5 !important; }
|
||
.comfy-markdown code { background: rgba(244,244,245,0.06) !important; border: 1px solid var(--linear-border) !important; border-radius: 4px !important; color: #a1a1aa !important; padding: 2px 5px !important; font-size: 12px !important; }
|
||
.comfy-markdown pre { background: var(--linear-surface-elevated) !important; border: 1px solid var(--linear-border) !important; border-radius: 8px !important; padding: 14px !important; }
|
||
.model_preview_top_container { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; }
|
||
/* NO background here: this container stacks ABOVE the mask editor's canvas
|
||
layers (z-index 1 vs 0), so an opaque bg hides the image entirely
|
||
(GitHub issue #2 "mask editor shows no image"). Color only. */
|
||
.maskEditor-ui-container { color: var(--linear-text-primary) !important; }
|
||
.graph-canvas-container { background: var(--bg-color, #0d0d0d) !important; }
|
||
.comfyui-body-top, .comfyui-body-bottom, .comfyui-body-left, .comfyui-body-right { background: transparent !important; }
|
||
.selection-toolbox { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; }
|
||
.litegraph-minimap { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 8px !important; opacity: 0.75 !important; }
|
||
.comfyui-split-button { border-radius: 7px !important; overflow: hidden !important; }
|
||
.comfyui-split-button .comfyui-split-primary { border-right: 1px solid var(--linear-border) !important; }
|
||
|
||
/* ── Sliders ── */
|
||
.p-slider { background: #222224 !important; border-radius: 4px !important; height: 4px !important; }
|
||
.p-slider .p-slider-range { background: #a1a1aa !important; }
|
||
.p-slider .p-slider-handle { background: #e4e4e7 !important; border: 2px solid #e4e4e7 !important; width: 14px !important; height: 14px !important; border-radius: 50% !important; font-size: 0 !important; line-height: 0 !important; text-indent: -9999px !important; overflow: hidden !important; cursor: pointer !important; }
|
||
.p-slider .p-slider-handle:hover { transform: scale(1.15) !important; }
|
||
|
||
/* ── Accordion ── */
|
||
.p-accordion-header { background: transparent !important; border: none !important; border-bottom: 1px solid var(--linear-border) !important; color: var(--linear-text-primary) !important; }
|
||
.p-accordion-header:hover { background: rgba(244,244,245,0.03) !important; }
|
||
.p-accordion-content { background: transparent !important; border: none !important; color: var(--linear-text-primary) !important; }
|
||
.p-autocomplete-panel { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; }
|
||
.p-autocomplete-item { color: var(--linear-text-primary) !important; }
|
||
.p-autocomplete-item:hover { background: rgba(244,244,245,0.04) !important; }
|
||
.p-splitter { background: transparent !important; border: none !important; }
|
||
.p-splitter-gutter { background: var(--linear-border) !important; }
|
||
.p-card { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; box-shadow: none !important; color: var(--linear-text-primary) !important; }
|
||
.p-divider { border-color: var(--linear-border) !important; }
|
||
.p-skeleton { background: var(--linear-surface-elevated) !important; }
|
||
*:focus-visible { outline: 2px solid rgba(161,161,170,0.3) !important; outline-offset: 2px !important; }
|
||
.p-progress-spinner-circle { stroke: #a1a1aa !important; }
|
||
.p-fieldset { background: transparent !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; }
|
||
.p-fieldset-legend { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 6px !important; color: var(--linear-text-primary) !important; }
|
||
.p-scrollpanel .p-scrollpanel-bar { background: #222224 !important; border-radius: 3px !important; }
|
||
.p-scrollpanel .p-scrollpanel-bar:hover { background: #3a3a41 !important; }
|
||
.p-confirmdialog { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 14px !important; }
|
||
.p-inputnumber-button { background: var(--linear-surface-elevated) !important; border: 1px solid var(--linear-border) !important; color: var(--linear-text-muted) !important; }
|
||
.p-inputnumber-button:hover { background: var(--button-surface-hover) !important; color: var(--linear-text-primary) !important; }
|
||
.p-radiobutton .p-radiobutton-box { background: var(--input-surface) !important; border: 1px solid #242429 !important; }
|
||
.p-radiobutton .p-radiobutton-box.p-highlight { background: var(--input-surface) !important; border-color: #e4e4e7 !important; }
|
||
.p-radiobutton .p-radiobutton-icon { background: #e4e4e7 !important; }
|
||
.p-textarea { background: var(--input-surface) !important; border: 1px solid var(--input-border) !important; border-radius: 7px !important; color: var(--linear-text-primary) !important; }
|
||
.p-breadcrumb { background: transparent !important; border: none !important; }
|
||
.p-breadcrumb-item-link { color: var(--linear-text-muted) !important; }
|
||
.p-breadcrumb-item-link:hover { color: var(--linear-text-primary) !important; }
|
||
.p-panel { background: #1a1a1a !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; }
|
||
.p-panel-header { background: var(--linear-surface) !important; border-bottom: 1px solid var(--linear-border) !important; color: var(--linear-text-primary) !important; }
|
||
.p-panel-content { background: #1a1a1a !important; color: var(--linear-text-primary) !important; }
|
||
/* Panels inside dialogs/settings keep their parent bg */
|
||
.p-dialog .p-panel, .p-dialog .p-panel-content, .settings-container .p-panel, .settings-container .p-panel-content { background: transparent !important; }
|
||
.p-inline-message { border-radius: 7px !important; }
|
||
.p-paginator { background: transparent !important; border: none !important; color: var(--linear-text-secondary) !important; gap: 2px !important; }
|
||
.p-paginator-page, .p-paginator-next, .p-paginator-prev, .p-paginator-first, .p-paginator-last { color: var(--linear-text-secondary) !important; border-radius: 6px !important; }
|
||
.p-paginator-page:hover, .p-paginator-next:hover, .p-paginator-prev:hover { background: rgba(244,244,245,0.05) !important; color: var(--linear-text-primary) !important; }
|
||
.p-paginator-page.p-highlight { background: rgba(244,244,245,0.08) !important; color: #f4f4f5 !important; }
|
||
|
||
/* ── ComfyUI Manager — Custom Nodes ── */
|
||
.cn-manager { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; color: #e4e4e7 !important; --grid-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; }
|
||
.cn-manager-header { gap: 10px !important; padding: 14px 16px !important; border-bottom: 1px solid #1c1c20 !important; }
|
||
.cn-manager-filter { background: #111113 !important; border: 1px solid #1c1c20 !important; border-radius: 7px !important; color: #8e8e99 !important; font-size: 13px !important; padding: 7px 28px 7px 10px !important; -webkit-appearance: none !important; appearance: none !important; background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%2348484f' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") !important; background-repeat: no-repeat !important; background-position: right 8px center !important; }
|
||
.cn-manager-filter:hover { border-color: #2c2c31 !important; color: #a1a1aa !important; filter: none !important; }
|
||
.cn-manager-filter:focus { border-color: #a1a1aa !important; outline: none !important; }
|
||
.cn-manager-keywords { background: #111113 !important; border: 1px solid #1c1c20 !important; border-radius: 7px !important; color: #e4e4e7 !important; font-size: 13px !important; padding: 7px 12px !important; flex: 1 !important; }
|
||
.cn-manager-keywords::placeholder { color: #3a3a41 !important; }
|
||
.cn-manager-keywords:hover { border-color: #2c2c31 !important; }
|
||
.cn-manager-keywords:focus { border-color: #a1a1aa !important; outline: none !important; }
|
||
.cn-manager-status { color: #3a3a41 !important; font-size: 12px !important; font-variant-numeric: tabular-nums !important; }
|
||
.cn-manager-grid { border: 1px solid #1c1c20 !important; border-radius: 10px !important; overflow: hidden !important; margin: 0 16px !important; }
|
||
.cn-manager-grid .tg-turbogrid { font-family: 'Inter', -apple-system, sans-serif !important; font-size: 13px !important; background: #0d0d0d !important; color: #e4e4e7 !important; }
|
||
.cn-manager-grid .tg-header-item { background: #111113 !important; border-bottom: 1px solid #1c1c20 !important; border-right: none !important; color: #48484f !important; font-size: 10px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; padding: 10px 12px !important; }
|
||
.cn-manager-grid .tg-column-name { color: inherit !important; }
|
||
.cn-manager-grid .tg-sort-indicator-icon { color: #63636e !important; }
|
||
.cn-manager-grid .tg-body { background: #0d0d0d !important; }
|
||
.cn-manager-grid .tg-row { border-bottom: 1px solid rgba(28,28,32,0.4) !important; }
|
||
.cn-manager-grid .tg-even { background: #0d0d0d !important; }
|
||
.cn-manager-grid .tg-odd { background: rgba(17,17,19,0.5) !important; }
|
||
.cn-manager-grid .tg-hover { background: rgba(244,244,245,0.025) !important; }
|
||
.cn-manager-grid .tg-cell { border-right: none !important; color: #8e8e99 !important; padding: 10px 12px !important; line-height: 1.5 !important; }
|
||
.cn-manager-grid .tg-cell-row-number { color: #242429 !important; font-size: 11px !important; font-variant-numeric: tabular-nums !important; }
|
||
.cn-manager-grid .tg-frozen-line-v { border-right: 1px solid #1c1c20 !important; }
|
||
.cn-manager-grid .tg-column-line { border-right: none !important; }
|
||
.cn-manager-grid .cn-pack-name a { color: #e4e4e7 !important; font-weight: 500 !important; text-decoration: none !important; }
|
||
.cn-manager-grid .cn-pack-name a:hover { color: #f4f4f5 !important; text-decoration: underline !important; text-underline-offset: 2px !important; }
|
||
.cn-manager-grid .cn-pack-desc { color: #63636e !important; font-size: 12px !important; line-height: 1.4 !important; }
|
||
.cn-manager-grid .cn-pack-desc a { color: #a1a1aa !important; text-decoration: underline !important; text-underline-offset: 2px !important; }
|
||
.cn-manager-grid .cn-pack-version { color: #48484f !important; font-variant-numeric: tabular-nums !important; font-size: 12px !important; }
|
||
.cn-manager-grid .cn-pack-nodes { color: #48484f !important; font-size: 12px !important; font-variant-numeric: tabular-nums !important; }
|
||
.cn-manager-grid .cn-pack-conflicts { color: #fbbf24 !important; font-size: 11px !important; font-weight: 500 !important; }
|
||
.cn-manager-grid .cn-pack-author { color: #63636e !important; font-size: 12px !important; }
|
||
.cn-manager-grid .cn-pack-stars { color: #3a3a41 !important; font-variant-numeric: tabular-nums !important; font-size: 11px !important; }
|
||
.cn-manager-grid .cn-pack-last-update { color: #3a3a41 !important; font-size: 11px !important; font-variant-numeric: tabular-nums !important; }
|
||
.cn-manager-grid .cn-pack-badge { background: rgba(244,244,245,0.04) !important; border: 1px solid rgba(244,244,245,0.06) !important; color: #48484f !important; font-size: 10px !important; font-weight: 500 !important; border-radius: 4px !important; padding: 2px 6px !important; }
|
||
.cn-manager-grid .tg-checkbox { border: none !important; background: transparent !important; }
|
||
.cn-manager-grid .tg-icon-checkbox { width: 16px !important; height: 16px !important; }
|
||
.cn-manager-grid .tg-checkbox-none { stroke: #3a3a41 !important; fill: none !important; }
|
||
.cn-manager-grid .tg-checkbox-selected { stroke: #e4e4e7 !important; fill: #e4e4e7 !important; }
|
||
.cn-manager-grid .tg-checkbox-mixed { stroke: #63636e !important; fill: #63636e !important; }
|
||
.cn-manager-grid .tg-turbogrid .tg-highlight::after { background-color: rgba(244,244,245,0.025) !important; }
|
||
.cn-manager-grid .tg-scrollbar-thumb { background: #222224 !important; border-radius: 3px !important; }
|
||
.cn-manager-grid .tg-scrollbar-thumb:hover, .cn-manager-grid .tg-scrollbar-thumb-hold { background: #3a3a41 !important; }
|
||
.cn-manager-grid .tg-scrollbar-track { background: transparent !important; }
|
||
.cn-manager-grid .tg-row-not-found { color: #3a3a41 !important; }
|
||
|
||
/* ── Manager Action Buttons ── */
|
||
.cn-manager .cn-install-buttons button { background: #111113 !important; border: 1px solid #222224 !important; border-radius: 6px !important; color: #8e8e99 !important; font-size: 11px !important; font-weight: 500 !important; padding: 4px 10px !important; min-width: 72px !important; filter: none !important; }
|
||
.cn-manager .cn-install-buttons button:hover { border-color: #2c2c31 !important; color: #e4e4e7 !important; background: #1a1a1c !important; filter: none !important; }
|
||
.cn-manager .cn-btn-install, .cn-manager .cn-btn-try-install, .cn-manager .cn-btn-update, .cn-manager .cn-btn-try-update, .cn-manager .cn-btn-enable, .cn-manager .cn-btn-disable, .cn-manager .cn-btn-switch, .cn-manager .cn-btn-try-fix, .cn-manager .cn-btn-reinstall { background: #111113 !important; color: #8e8e99 !important; }
|
||
.cn-manager .cn-btn-install:hover, .cn-manager .cn-btn-try-install:hover, .cn-manager .cn-btn-update:hover, .cn-manager .cn-btn-try-update:hover, .cn-manager .cn-btn-enable:hover, .cn-manager .cn-btn-disable:hover, .cn-manager .cn-btn-switch:hover, .cn-manager .cn-btn-try-fix:hover, .cn-manager .cn-btn-reinstall:hover { color: #e4e4e7 !important; }
|
||
.cn-manager .cn-btn-uninstall { background: #111113 !important; border-color: rgba(248,113,113,0.2) !important; color: #63636e !important; }
|
||
.cn-manager .cn-btn-uninstall:hover { border-color: rgba(248,113,113,0.5) !important; color: #f87171 !important; background: rgba(248,113,113,0.06) !important; }
|
||
.cn-manager button.cn-btn-loading { border-color: rgba(161,161,170,0.3) !important; background: #111113 !important; }
|
||
.cn-manager button.cn-btn-loading::after { background-image: repeating-linear-gradient(-45deg, rgba(161,161,170,0.1), rgba(161,161,170,0.1) 10px, transparent 10px, transparent 15px) !important; }
|
||
.cn-manager .cn-btn-import-failed { background: #111113 !important; border-color: rgba(248,113,113,0.25) !important; color: #f87171 !important; font-size: 10px !important; }
|
||
|
||
/* ── Manager Footer ── */
|
||
.cn-manager-footer { gap: 8px !important; padding: 12px 16px !important; border-top: 1px solid #1c1c20 !important; }
|
||
.cn-manager-footer button { background: #111113 !important; border: 1px solid #222224 !important; border-radius: 7px !important; color: #63636e !important; font-size: 12px !important; font-weight: 500 !important; padding: 7px 14px !important; filter: none !important; }
|
||
.cn-manager-footer button:hover { border-color: #2c2c31 !important; color: #e4e4e7 !important; background: #1a1a1c !important; filter: none !important; }
|
||
.cn-manager .cn-manager-restart { border-color: rgba(248,113,113,0.2) !important; color: #f87171 !important; background: #111113 !important; }
|
||
.cn-manager .cn-manager-restart:hover { border-color: rgba(248,113,113,0.5) !important; background: rgba(248,113,113,0.06) !important; }
|
||
.cn-manager-selection { gap: 8px !important; }
|
||
.cn-selected-buttons button { font-size: 12px !important; }
|
||
|
||
/* ── Manager Flyover ── */
|
||
.cn-flyover { background-color: #0e0e10 !important; border-left: 1px solid #1c1c20 !important; }
|
||
.cn-flyover::before { background-image: none !important; }
|
||
.cn-flyover-header { border-bottom: 1px solid #1c1c20 !important; color: #e4e4e7 !important; padding: 14px 16px !important; }
|
||
.cn-flyover-title { color: #e4e4e7 !important; font-size: 14px !important; font-weight: 600 !important; }
|
||
.cn-flyover-close { color: #63636e !important; }
|
||
.cn-flyover-close:hover { color: #e4e4e7 !important; }
|
||
.cn-flyover-body { background-color: #0e0e10 !important; }
|
||
.cn-nodes-row { border-bottom: 1px solid rgba(28,28,32,0.4) !important; padding: 6px 12px !important; }
|
||
.cn-nodes-row:nth-child(odd) { background-color: rgba(17,17,19,0.4) !important; }
|
||
.cn-nodes-row:hover { background-color: rgba(244,244,245,0.025) !important; }
|
||
.cn-nodes-sn { color: #3a3a41 !important; }
|
||
.cn-nodes-name { color: #e4e4e7 !important; }
|
||
.cn-nodes-pack { color: #8e8e99 !important; }
|
||
.cn-nodes-conflict .cn-nodes-name, .cn-nodes-conflict .cn-icon { color: #fbbf24 !important; }
|
||
.cn-popover { background: #0e0e10 !important; border: 1px solid #1c1c20 !important; border-radius: 10px !important; filter: none !important; }
|
||
.cn-preview { color: #e4e4e7 !important; }
|
||
.cn-preview-header { border-bottom: 1px solid #1c1c20 !important; }
|
||
.cn-preview-name { color: #e4e4e7 !important; font-weight: 600 !important; }
|
||
.cn-preview-value { color: #63636e !important; }
|
||
.cn-preview-string { background: #151517 !important; color: #8e8e99 !important; border-radius: 5px !important; }
|
||
.cn-preview-switch { background: #151517 !important; border: 1px solid #222224 !important; border-radius: 6px !important; }
|
||
.cn-preview-description { background: #151517 !important; color: #63636e !important; border-radius: 6px !important; }
|
||
.cn-tag-list > div { background-color: rgba(244,244,245,0.04) !important; border-radius: 4px !important; color: #63636e !important; font-size: 11px !important; }
|
||
|
||
/* ── Manager Version Selector ── */
|
||
.comfy-modal[style*="1100"], .comfy-modal[style*="z-index"] { background: #0e0e10 !important; border: 1px solid #1c1c20 !important; border-radius: 12px !important; color: #e4e4e7 !important; overflow: hidden !important; }
|
||
.comfy-modal[style*="1100"] select, .comfy-modal select { background: #151517 !important; border: 1px solid #222224 !important; border-radius: 7px !important; color: #e4e4e7 !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; font-size: 13px !important; }
|
||
.comfy-modal[style*="1100"] select:focus, .comfy-modal select:focus { border-color: #a1a1aa !important; outline: none !important; }
|
||
.comfy-modal[style*="1100"] select option, .comfy-modal select option { background: #151517 !important; color: #e4e4e7 !important; }
|
||
.comfy-modal[style*="1100"] button, .comfy-modal button { background: #111113 !important; border: 1px solid #222224 !important; border-radius: 7px !important; color: #8e8e99 !important; font-size: 13px !important; font-weight: 500 !important; }
|
||
.comfy-modal[style*="1100"] button:hover, .comfy-modal button:hover { border-color: #2c2c31 !important; color: #e4e4e7 !important; background: #1a1a1c !important; }
|
||
.cn-manager button:disabled, .cn-manager input:disabled, .cn-manager select:disabled { color: #3a3a41 !important; opacity: 0.45 !important; cursor: not-allowed !important; }
|
||
.cn-manager button { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; filter: none !important; }
|
||
.cn-manager button:hover { filter: none !important; }
|
||
|
||
/* ── Actionbar ── */
|
||
.actionbar .p-panel-content { background: #131313 !important; border: none !important; border-radius: 10px !important; padding: 4px 6px !important; }
|
||
.actionbar.p-0 .p-panel-content, .actionbar.border-none .p-panel-content { border: none !important; background: #131313 !important; border-radius: 10px !important; }
|
||
.actionbar .p-panel-content .flex > * { height: 32px !important; display: inline-flex !important; align-items: center !important; }
|
||
.actionbar .p-inputnumber .p-inputnumber-input, .actionbar .p-inputnumber .p-inputnumber-button, .actionbar .p-inputnumber-input, .actionbar .p-inputnumber-button, .actionbar .p-inputnumber-increment-button, .actionbar .p-inputnumber-decrement-button { border: 0 !important; border-width: 0 !important; border-style: none !important; }
|
||
.actionbar .drag-handle { opacity: 0.2 !important; }
|
||
.actionbar .drag-handle:hover { opacity: 0.5 !important; }
|
||
.comfyui-queue-button .p-splitbutton { border-radius: 7px !important; }
|
||
.comfyui-queue-button .p-splitbutton-button, .comfyui-queue-button .p-splitbutton-dropdown { background: rgba(244,244,245,0.05) !important; border: 1px solid #222224 !important; color: #e4e4e7 !important; font-size: 13px !important; font-weight: 500 !important; height: 32px !important; }
|
||
.comfyui-queue-button .p-splitbutton-button { border-radius: 7px 0 0 7px !important; padding: 0 14px !important; gap: 6px !important; }
|
||
.comfyui-queue-button .p-splitbutton-dropdown { border-radius: 0 7px 7px 0 !important; border-left: none !important; padding: 0 8px !important; color: #63636e !important; }
|
||
.comfyui-queue-button .p-splitbutton-button:hover { background: rgba(244,244,245,0.09) !important; color: #f4f4f5 !important; }
|
||
.comfyui-queue-button .p-splitbutton-dropdown:hover { background: rgba(244,244,245,0.07) !important; color: #e4e4e7 !important; }
|
||
.comfyui-queue-button .p-splitbutton-button .p-button-label { color: inherit !important; font-weight: 500 !important; }
|
||
.comfyui-queue-button .p-splitbutton-button .p-button-icon { color: #63636e !important; font-size: 14px !important; }
|
||
.actionbar button[variant="destructive"], .actionbar .p-button-destructive, .actionbar button:has(.icon-\\[lucide--x\\]) { background: rgba(14,14,16,0.5) !important; border: 1px solid rgba(248,113,113,0.15) !important; border-radius: 7px !important; color: #48484f !important; width: 32px !important; height: 32px !important; padding: 0 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; }
|
||
.actionbar button[variant="destructive"]:hover, .actionbar .p-button-destructive:hover, .actionbar button:has(.icon-\\[lucide--x\\]):hover { border-color: rgba(248,113,113,0.4) !important; color: #f87171 !important; background: rgba(248,113,113,0.06) !important; }
|
||
.actionbar button[variant="destructive"]:disabled, .actionbar .p-button-destructive:disabled, .actionbar button:has(.icon-\\[lucide--x\\]):disabled { border-color: #1c1c20 !important; color: #242429 !important; opacity: 0.45 !important; }
|
||
/* Top bar buttons — all uniform, no colored backgrounds */
|
||
.comfyui-button.primary, button.comfyui-button.primary, .comfyui-menu-mobile-collapse.primary,
|
||
.actionbar-container .comfyui-button, .actionbar-container button.comfyui-button,
|
||
.actionbar-container .comfy-menu-mobile-collapse { background: transparent !important; border: none !important; border-radius: 7px !important; color: #48484f !important; font-size: 13px !important; font-weight: 500 !important; height: 32px !important; padding: 0 14px !important; }
|
||
.comfyui-button.primary:hover, button.comfyui-button.primary:hover, .comfyui-menu-mobile-collapse.primary:hover,
|
||
.actionbar-container .comfyui-button:hover, .actionbar-container button.comfyui-button:hover { border: none !important; color: #e4e4e7 !important; background: rgba(244,244,245,0.06) !important; }
|
||
|
||
/* Run button — subtle white, not blue */
|
||
.actionbar-container button[class*="Run"],
|
||
.actionbar button[class*="run"],
|
||
button.comfyui-button[style*="background"],
|
||
.actionbar-container [style*="0B8CE9"],
|
||
.actionbar-container [style*="background-color"] { background: rgba(244,244,245,0.08) !important; border: none !important; color: #e4e4e7 !important; }
|
||
.actionbar-container button[class*="Run"]:hover,
|
||
.actionbar button[class*="run"]:hover { background: rgba(244,244,245,0.12) !important; color: #ffffff !important; }
|
||
|
||
/* Favorite star + all icon buttons in top bar */
|
||
.actionbar-container .comfyui-button[style*="background"],
|
||
.actionbar-container button[style*="background"] { background: transparent !important; border: none !important; }
|
||
.actionbar-container .comfyui-button[style*="background"]:hover,
|
||
.actionbar-container button[style*="background"]:hover { background: rgba(244,244,245,0.06) !important; }
|
||
.actionbar .p-button-text, .actionbar .p-button-icon-only:not(.p-splitbutton-dropdown):not(.p-button-destructive):not(:has(.icon-\\[lucide--x\\])), .actionbar button[size="icon"]:not(:has(.icon-\\[lucide--x\\])) { background: transparent !important; border: 1px solid transparent !important; border-radius: 7px !important; color: #48484f !important; width: 32px !important; height: 32px !important; padding: 0 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; }
|
||
.actionbar .p-button-text:hover, .actionbar .p-button-icon-only:not(.p-splitbutton-dropdown):not(.p-button-destructive):hover, .actionbar button[size="icon"]:not(:has(.icon-\\[lucide--x\\])):hover { color: #e4e4e7 !important; background: rgba(244,244,245,0.05) !important; border-color: transparent !important; }
|
||
.actionbar .p-inputnumber { height: 32px !important; }
|
||
.actionbar .p-inputnumber .p-inputnumber-input { background: rgba(14,14,16,0.5) !important; border: none !important; border-radius: 5px 0 0 5px !important; color: #8e8e99 !important; font-size: 12px !important; font-variant-numeric: tabular-nums !important; height: 32px !important; width: 32px !important; text-align: center !important; padding: 0 !important; }
|
||
.actionbar .p-inputnumber .p-inputnumber-input:focus { border: none !important; color: #e4e4e7 !important; }
|
||
.actionbar .p-inputnumber-button { background: rgba(14,14,16,0.5) !important; border: none !important; color: #48484f !important; width: 16px !important; height: 16px !important; padding: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; }
|
||
.actionbar .p-inputnumber-button .p-icon, .actionbar .p-inputnumber-button svg, .actionbar .p-inputnumber-button i { width: 10px !important; height: 10px !important; font-size: 10px !important; }
|
||
.actionbar .p-inputnumber-button:hover { color: #e4e4e7 !important; background: rgba(244,244,245,0.04) !important; }
|
||
.actionbar .p-inputnumber-button-up { border-radius: 0 5px 0 0 !important; }
|
||
.actionbar .p-inputnumber-button-down { border-radius: 0 0 5px 0 !important; }
|
||
.actionbar span[class*="active"], .actionbar .queue-count, .actionbar .p-panel-content .flex span { color: #48484f !important; font-size: 12px !important; font-weight: 500 !important; font-variant-numeric: tabular-nums !important; white-space: nowrap !important; }
|
||
.actionbar [class*="status"], .actionbar [class*="indicator"] { font-size: 12px !important; font-weight: 500 !important; color: #48484f !important; }
|
||
.actionbar [class*="running"], .actionbar [class*="active"]:not(button) { color: #4ade80 !important; }
|
||
.actionbar [class*="error"] { color: #f87171 !important; }
|
||
.actionbar [class*="separator"], .actionbar [class*="divider"], .actionbar hr { background: #1c1c20 !important; border-color: #1c1c20 !important; width: 1px !important; margin: 4px 2px !important; }
|
||
.comfyui-queue-button .p-menu, .p-splitbutton-menu, .p-tieredmenu { background: #0e0e10 !important; border: 1px solid #1c1c20 !important; border-radius: 10px !important; padding: 4px !important; }
|
||
.comfyui-queue-button .p-menuitem-link, .p-splitbutton-menu .p-menuitem-link, .p-tieredmenu .p-menuitem-link { border-radius: 6px !important; padding: 8px 12px !important; color: #8e8e99 !important; font-size: 13px !important; }
|
||
.comfyui-queue-button .p-menuitem-link:hover, .p-splitbutton-menu .p-menuitem-link:hover, .p-tieredmenu .p-menuitem-link:hover { background: rgba(244,244,245,0.05) !important; color: #e4e4e7 !important; }
|
||
|
||
/* ── Manager Main Dialog ── */
|
||
#cm-manager-dialog { background: #0e0e10 !important; border: 1px solid #1c1c20 !important; border-radius: 14px !important; box-shadow: none !important; color: #e4e4e7 !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; overflow: hidden !important; }
|
||
.cm-menu-container { gap: 20px !important; padding: 24px !important; }
|
||
.cm-menu-column { gap: 10px !important; }
|
||
.cm-menu-column > p, .cm-menu-column > span, #cm-manager-dialog p:not(:empty) { color: #48484f !important; font-size: 10px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; }
|
||
#cm-manager-dialog label, #cm-manager-dialog td { color: #63636e !important; font-size: 12px !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; }
|
||
.cm-menu-combo, #cm-manager-dialog select { background: #111113 !important; border: 1px solid #1c1c20 !important; border-radius: 7px !important; color: #8e8e99 !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; font-size: 13px !important; padding: 8px 32px 8px 12px !important; cursor: pointer !important; width: 100% !important; -webkit-appearance: none !important; appearance: none !important; background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2348484f' 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") !important; background-repeat: no-repeat !important; background-position: right 10px center !important; }
|
||
.cm-menu-combo:hover, #cm-manager-dialog select:hover { border-color: #2c2c31 !important; color: #a1a1aa !important; }
|
||
.cm-menu-combo:focus, #cm-manager-dialog select:focus { border-color: #a1a1aa !important; outline: none !important; }
|
||
#cm-manager-dialog select option { background: #151517 !important; color: #e4e4e7 !important; }
|
||
.cm-button, #cm-manager-dialog button { background: #1a1a1a !important; border: 1px solid transparent !important; border-radius: 8px !important; color: #e4e4e7 !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; font-size: 13px !important; font-weight: 500 !important; padding: 9px 16px !important; cursor: pointer !important; filter: none !important; text-align: left !important; width: 100% !important; }
|
||
.cm-button:hover, #cm-manager-dialog button:hover { background: #222226 !important; border-color: #2a2a2e !important; color: #ffffff !important; filter: none !important; }
|
||
.cm-small-button { font-size: 12px !important; padding: 5px 12px !important; }
|
||
.cm-button-red, #cm-manager-dialog .cm-button-red { background: #111113 !important; border-color: rgba(248,113,113,0.2) !important; color: #63636e !important; }
|
||
.cm-button-red:hover, #cm-manager-dialog .cm-button-red:hover { border-color: rgba(248,113,113,0.5) !important; color: #f87171 !important; background: rgba(248,113,113,0.05) !important; }
|
||
.cm-button-orange, #cm-manager-dialog .cm-button-orange { background: #111113 !important; border: 1px solid #1c1c20 !important; color: #8e8e99 !important; }
|
||
.cm-button-orange:hover, #cm-manager-dialog .cm-button-orange:hover { border-color: #2c2c31 !important; color: #e4e4e7 !important; background: #1a1a1c !important; }
|
||
.cm-experimental { border: 1px solid rgba(28,28,32,0.6) !important; border-radius: 10px !important; padding: 14px !important; background: rgba(17,17,19,0.4) !important; }
|
||
.cm-experimental-legend { color: #3a3a41 !important; font-size: 10px !important; text-transform: uppercase !important; letter-spacing: 0.08em !important; font-weight: 600 !important; background: #0e0e10 !important; padding: 2px 10px !important; }
|
||
.cm-experimental-button { font-size: 12px !important; }
|
||
.cm-notice-board { background: #111113 !important; border: 1px solid #1c1c20 !important; border-radius: 10px !important; color: #63636e !important; font-size: 12px !important; padding: 14px 16px !important; line-height: 1.6 !important; }
|
||
.cm-notice-board h3, .cm-notice-board b, .cm-notice-board strong { color: #e4e4e7 !important; }
|
||
.cm-notice-board hr { border-color: #1c1c20 !important; }
|
||
.cm-notice-board a { color: #a1a1aa !important; text-decoration: underline !important; text-underline-offset: 2px !important; }
|
||
#cm-manager-dialog .comfy-modal-content > h2, #cm-manager-dialog h2, #cm-manager-dialog h3 { color: #e4e4e7 !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; font-weight: 600 !important; letter-spacing: -0.01em !important; }
|
||
#cm-manager-dialog table { border-collapse: collapse !important; }
|
||
#cm-manager-dialog table td { padding: 5px 10px !important; border: none !important; }
|
||
#cm-manager-dialog .p-dialog-header-close, #cm-manager-dialog > button:last-child { color: #63636e !important; }
|
||
#cm-manager-dialog .p-dialog-header-close:hover, #cm-manager-dialog > button:last-child:hover { color: #e4e4e7 !important; }
|
||
.cm-share-menu, .cm-dropdown-menu { background: #0e0e10 !important; border: 1px solid #1c1c20 !important; border-radius: 10px !important; padding: 4px !important; }
|
||
.cm-share-menu a, .cm-share-menu button, .cm-dropdown-menu a, .cm-dropdown-menu button { background: transparent !important; border: none !important; border-radius: 6px !important; color: #8e8e99 !important; padding: 8px 12px !important; display: block !important; width: 100% !important; text-align: left !important; }
|
||
.cm-share-menu a:hover, .cm-share-menu button:hover, .cm-dropdown-menu a:hover, .cm-dropdown-menu button:hover { background: rgba(244,244,245,0.05) !important; color: #e4e4e7 !important; }
|
||
.manager-dialog, #cm-manager-dialog { background: #0e0e10 !important; border: 1px solid #1c1c20 !important; border-radius: 14px !important; overflow: hidden !important; }
|
||
|
||
/* ── Canvas Bottom Toolbar ── */
|
||
[class*="z-1200"].p-buttongroup, [class*="z-1200"][class*="bg-comfy-menu-bg"], .p-buttongroup[class*="bottom-0"][class*="right-0"][class*="z-1200"] { background: #1a1a1a !important; border-color: var(--linear-border) !important; border-radius: 10px !important; overflow: hidden !important; }
|
||
[class*="z-1200"] > button, [class*="z-1200"] .p-button { background: transparent !important; border: none !important; color: #48484f !important; }
|
||
[class*="z-1200"] > button:hover, [class*="z-1200"] .p-button:hover { color: #e4e4e7 !important; background: rgba(244,244,245,0.05) !important; }
|
||
[class*="z-1200"] [class*="bg-node-divider"], [class*="z-1200"] > div[class*="w-\\[1px\\]"] { background: var(--linear-border) !important; }
|
||
|
||
/* ── Node Library Sidebar ── */
|
||
.comfy-vue-side-bar-header { background: transparent !important; border-bottom: 1px solid var(--linear-border) !important; padding: 10px 14px !important; }
|
||
.comfy-vue-side-bar-header .p-toolbar { background: transparent !important; border: none !important; padding: 0 !important; min-height: unset !important; }
|
||
.comfy-vue-side-bar-header .p-toolbar .p-button { background: transparent !important; border: none !important; color: #48484f !important; width: 28px !important; height: 28px !important; padding: 0 !important; border-radius: 6px !important; }
|
||
.comfy-vue-side-bar-header .p-toolbar .p-button:hover { color: #e4e4e7 !important; background: rgba(244,244,245,0.05) !important; }
|
||
.node-lib-search-box { background: transparent !important; padding: 10px 14px !important; }
|
||
.node-lib-search-box input, .node-lib-search-box .p-inputtext { background: var(--input-surface) !important; border: 1px solid var(--input-border) !important; border-radius: 7px !important; color: var(--linear-text-primary) !important; font-size: 13px !important; height: 34px !important; }
|
||
.node-lib-search-box input:focus, .node-lib-search-box .p-inputtext:focus { border-color: var(--linear-accent-secondary) !important; }
|
||
.node-lib-tree-explorer { background: transparent !important; }
|
||
.node-lib-tree-explorer .tree-explorer { background: transparent !important; }
|
||
.node-lib-tree-explorer .p-tree { background: transparent !important; border: none !important; padding: 4px 8px !important; }
|
||
.node-lib-tree-explorer .p-tree-node-content { border-radius: 6px !important; padding: 5px 8px !important; margin: 1px 0 !important; }
|
||
.node-lib-tree-explorer .p-tree-node-content:hover { background: rgba(244,244,245,0.035) !important; }
|
||
.node-lib-tree-explorer .p-tree-node-content.p-highlight, .node-lib-tree-explorer .p-tree-node.p-tree-node-selected > .p-tree-node-content { background: rgba(244,244,245,0.07) !important; }
|
||
.node-lib-tree-explorer .p-tree-node-label { color: #8e8e99 !important; font-size: 13px !important; font-weight: 400 !important; }
|
||
.node-lib-tree-explorer .p-tree-node-content:hover .p-tree-node-label, .node-lib-tree-explorer .p-tree-node-content.p-highlight .p-tree-node-label { color: #e4e4e7 !important; }
|
||
.node-lib-tree-explorer .p-tree-node-toggle-button, .node-lib-tree-explorer .p-tree-toggler { color: #3a3a41 !important; width: 20px !important; height: 20px !important; background: transparent !important; border: none !important; }
|
||
.node-lib-tree-explorer .p-tree-node-toggle-button:hover, .node-lib-tree-explorer .p-tree-toggler:hover { color: #63636e !important; background: transparent !important; }
|
||
.node-lib-tree-explorer .p-tree-node-icon { color: #48484f !important; font-size: 14px !important; }
|
||
.node-lib-tree-explorer .p-tree-node-content:hover .p-tree-node-icon { color: #63636e !important; }
|
||
.leaf-count-badge { background: rgba(244,244,245,0.04) !important; color: #3a3a41 !important; font-size: 10px !important; font-weight: 500 !important; border-radius: 4px !important; padding: 1px 6px !important; border: none !important; min-width: unset !important; }
|
||
.node-lib-node-preview, #node-library-node-preview-container { background: #111113 !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; color: var(--linear-text-primary) !important; overflow: hidden !important; }
|
||
.node-lib-bookmark-tree-explorer { background: transparent !important; }
|
||
.node-lib-bookmark-tree-explorer .p-tree { background: transparent !important; border: none !important; }
|
||
|
||
/* ── Sidebar Panels ── */
|
||
.side-bar-panel [class*="panel-header"], .side-bar-panel [class*="sidebar-header"], .side-bar-panel > div:first-child { border-bottom: 1px solid #1c1c20 !important; }
|
||
.side-bar-panel h3, .side-bar-panel [class*="panel-title"] { color: #e4e4e7 !important; font-size: 13px !important; font-weight: 600 !important; }
|
||
.side-bar-panel [class*="list-item"], .side-bar-panel [class*="tree-item"], .side-bar-panel .p-listbox-option { border-radius: 6px !important; color: #8e8e99 !important; font-size: 13px !important; margin: 1px 4px !important; padding: 6px 10px !important; }
|
||
.side-bar-panel [class*="list-item"]:hover, .side-bar-panel [class*="tree-item"]:hover, .side-bar-panel .p-listbox-option:hover { background: rgba(244,244,245,0.035) !important; color: #e4e4e7 !important; }
|
||
.side-bar-panel [class*="list-item"].active, .side-bar-panel [class*="list-item"][aria-selected="true"], .side-bar-panel .p-listbox-option.p-highlight { background: rgba(244,244,245,0.07) !important; color: #f4f4f5 !important; }
|
||
.side-bar-panel [class*="section-divider"], .side-bar-panel hr { border-color: #1c1c20 !important; margin: 8px 0 !important; }
|
||
.side-bar-panel [class*="empty-state"], .side-bar-panel [class*="no-results"] { color: #3a3a41 !important; font-size: 13px !important; text-align: center !important; padding: 32px 16px !important; }
|
||
.side-bar-panel [class*="model-item"], .side-bar-panel [class*="model-card"] { background: transparent !important; border: 1px solid transparent !important; border-radius: 8px !important; }
|
||
.side-bar-panel [class*="model-item"]:hover, .side-bar-panel [class*="model-card"]:hover { background: rgba(244,244,245,0.025) !important; border-color: #1c1c20 !important; }
|
||
.side-bar-panel [class*="thumbnail"], .side-bar-panel [class*="preview-image"] { border-radius: 6px !important; border: 1px solid #1c1c20 !important; }
|
||
.side-bar-panel [class*="workflow-item"], .side-bar-panel [class*="workflow-entry"] { border-bottom: 1px solid rgba(28,28,32,0.3) !important; padding: 10px 12px !important; }
|
||
.side-bar-panel [class*="workflow-item"]:hover { background: rgba(244,244,245,0.025) !important; }
|
||
.side-bar-panel .p-toolbar .p-button, .side-bar-panel [class*="toolbar"] button { background: transparent !important; border: none !important; color: #48484f !important; border-radius: 6px !important; width: 28px !important; height: 28px !important; padding: 0 !important; }
|
||
.side-bar-panel .p-toolbar .p-button:hover, .side-bar-panel [class*="toolbar"] button:hover { background: rgba(244,244,245,0.05) !important; color: #e4e4e7 !important; }
|
||
.comfyui-menu, [class*="top-bar"], .top-menubar { background: #0d0d0d !important; border-bottom: 1px solid #1c1c20 !important; }
|
||
|
||
/* ── Templates ── */
|
||
.workflow-template-selector, [class*="template-selector"], .p-dialog:has([class*="template"]) .p-dialog-content { background: var(--linear-surface) !important; color: var(--linear-text-primary) !important; }
|
||
.workflow-template-selector .left-side-panel, [class*="template"] .left-side-panel, .left-side-panel { background: transparent !important; border-right: 1px solid var(--linear-border) !important; }
|
||
.left-side-panel .p-listbox { background: transparent !important; border: none !important; }
|
||
.left-side-panel .p-listbox-option { border-radius: 6px !important; color: var(--linear-text-secondary) !important; margin: 1px 4px !important; }
|
||
.left-side-panel .p-listbox-option:hover { background: rgba(244,244,245,0.035) !important; color: var(--linear-text-primary) !important; }
|
||
.left-side-panel .p-listbox-option.p-highlight, .left-side-panel .p-listbox-option.p-selected { background: rgba(244,244,245,0.07) !important; color: #f4f4f5 !important; }
|
||
[class*="template"] .p-card, .workflow-template-card, [class*="variant-ghost"][class*="rounded-lg"] { background: var(--linear-surface) !important; border: 1px solid var(--linear-border) !important; border-radius: 10px !important; overflow: hidden !important; }
|
||
[class*="template"] .p-card:hover, .workflow-template-card:hover, [class*="variant-ghost"][class*="rounded-lg"]:hover { border-color: var(--linear-border-hover) !important; }
|
||
[class*="template"] .p-card img, .workflow-template-card img { border-radius: 8px !important; opacity: 0.85 !important; }
|
||
[class*="template"] .p-card:hover img, .workflow-template-card:hover img { opacity: 1 !important; }
|
||
[class*="template"] .p-card .p-card-title, .workflow-template-card [class*="font-semibold"] { color: var(--linear-text-primary) !important; font-weight: 500 !important; }
|
||
[class*="template"] .p-card .p-card-subtitle, .workflow-template-card [class*="text-muted"] { color: var(--linear-text-muted) !important; font-size: 12px !important; }
|
||
[class*="template"] .p-iconfield, .workflow-template-selector .p-iconfield { background: transparent !important; }
|
||
[class*="template"] .p-multiselect, [class*="template"] .p-select { background: var(--input-surface) !important; border: 1px solid var(--input-border) !important; border-radius: 7px !important; color: var(--linear-text-primary) !important; font-size: 13px !important; }
|
||
[class*="template"] .p-multiselect:hover, [class*="template"] .p-select:hover { border-color: var(--linear-border-hover) !important; }
|
||
[class*="template"] .square-chip, .square-chip { background: rgba(244,244,245,0.05) !important; border: 1px solid rgba(244,244,245,0.06) !important; border-radius: 4px !important; color: var(--linear-text-muted) !important; font-size: 11px !important; }
|
||
[class*="template"] .p-skeleton, [class*="template"] [class*="animate-pulse"] { background: var(--linear-surface-elevated) !important; border-radius: 6px !important; }
|
||
[class*="template"] [class*="text-neutral"], [class*="template"] [class*="text-muted"] { color: var(--linear-text-muted) !important; }
|
||
[class*="template"] [class*="text-2xl"], [class*="template"] [class*="font-semibold"] { color: var(--linear-text-primary) !important; }
|
||
|
||
/* ── Bottom Panel — Keyboard Shortcuts ── */
|
||
.bottom-panel .p-tablist { border-bottom: none !important; }
|
||
.bottom-panel .p-tab { font-size: 12px !important; font-weight: 500 !important; text-transform: uppercase !important; letter-spacing: 0.04em !important; color: #48484f !important; padding: 10px 16px !important; }
|
||
.bottom-panel .p-tab:hover { color: #8e8e99 !important; }
|
||
.bottom-panel .p-tab-active, .bottom-panel .p-tab[data-p-active="true"] { color: #e4e4e7 !important; }
|
||
.bottom-panel th, .bottom-panel [class*="section-title"], .bottom-panel [class*="column-header"] { color: #48484f !important; font-size: 10px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; padding: 8px 12px !important; border-bottom: 1px solid #1c1c20 !important; }
|
||
.bottom-panel td, .bottom-panel [class*="shortcut-label"], .bottom-panel [class*="command-name"] { color: #8e8e99 !important; font-size: 12px !important; padding: 6px 12px !important; }
|
||
.bottom-panel kbd, .bottom-panel [class*="keycap"], .bottom-panel [class*="key-badge"], .bottom-panel [class*="keyboard"], .bottom-panel [class*="shortcut"] code, .bottom-panel [class*="key"] { background: rgba(244,244,245,0.05) !important; border: 1px solid rgba(244,244,245,0.07) !important; border-radius: 4px !important; color: #a1a1aa !important; font-size: 11px !important; font-weight: 500 !important; padding: 2px 6px !important; font-family: 'Inter', monospace !important; }
|
||
.bottom-panel [class*="queue-item"], .bottom-panel [class*="job-item"] { border-bottom: 1px solid rgba(28,28,32,0.3) !important; padding: 8px 16px !important; }
|
||
.bottom-panel [class*="queue-item"]:hover, .bottom-panel [class*="job-item"]:hover { background: rgba(244,244,245,0.025) !important; }
|
||
|
||
/* ── Override Tailwind primary blue → white ── */
|
||
.bg-primary-background { background-color: rgba(244,244,245,0.1) !important; }
|
||
.hover\:bg-primary-background-hover:hover, .bg-primary-background:hover { background-color: rgba(244,244,245,0.15) !important; }
|
||
.bg-secondary-background { background-color: #131313 !important; }
|
||
.hover\:bg-secondary-background-hover:hover, .bg-secondary-background:hover { background-color: rgba(244,244,245,0.06) !important; }
|
||
|
||
/* ── Confirm Dialog ── */
|
||
.p-confirmdialog .p-dialog-header { padding: 20px 24px 12px !important; }
|
||
.p-confirmdialog .p-confirmdialog-message { color: #8e8e99 !important; font-size: 14px !important; line-height: 1.6 !important; }
|
||
.p-confirmdialog .p-dialog-footer { gap: 8px !important; padding: 16px 24px !important; }
|
||
|
||
/* ── Actionbar fix — override Tailwind bg-transparent ── */
|
||
.p-panel.actionbar,
|
||
.p-panel.actionbar.bg-transparent,
|
||
.actionbar.bg-transparent {
|
||
background: #131313 !important;
|
||
border: none !important;
|
||
border-radius: 10px !important;
|
||
}
|
||
.p-panel.actionbar .p-panel-content,
|
||
.actionbar.bg-transparent .p-panel-content {
|
||
background: #131313 !important;
|
||
border: none !important;
|
||
border-radius: 10px !important;
|
||
}
|
||
|
||
/* ══════════════════════════════════════
|
||
v2.0 — Live session fixes
|
||
══════════════════════════════════════ */
|
||
|
||
/* Panel backgrounds — solid */
|
||
.bg-comfy-menu-bg { background-color: #131313 !important; }
|
||
.bg-base-background { background: #131313 !important; }
|
||
|
||
/* Tailwind primary blue → white */
|
||
.bg-primary-background { background-color: rgba(244,244,245,0.15) !important; }
|
||
.hover\:bg-primary-background-hover:hover { background-color: rgba(244,244,245,0.20) !important; }
|
||
.hover\:bg-secondary-background-hover:hover { background-color: rgba(244,244,245,0.08) !important; }
|
||
.hover\:bg-destructive-background-hover:hover { background-color: rgba(248,113,113,0.1) !important; }
|
||
|
||
/* All bars — icons/text white */
|
||
.side-tool-bar-container button { color: #a1a1aa !important; }
|
||
.side-tool-bar-container button:hover { color: #ffffff !important; }
|
||
.side-tool-bar-container button.active, .side-tool-bar-container button[aria-selected="true"] { color: #ffffff !important; }
|
||
.graph-canvas-panel button { color: #e4e4e7 !important; }
|
||
.graph-canvas-panel button:hover { color: #ffffff !important; }
|
||
.graph-canvas-panel span { color: #e4e4e7 !important; }
|
||
|
||
/* Canvas toolbar — no active button bg */
|
||
.graph-canvas-panel .bg-interface-panel-selected-surface, .graph-canvas-panel [class*="selected-surface"] { background: transparent !important; color: #ffffff !important; }
|
||
|
||
/* Tooltips — fix */
|
||
.p-tooltip { max-width: 250px !important; min-width: 120px !important; background: #131313 !important; border: 1px solid #1e1e22 !important; border-radius: 8px !important; color: #e4e4e7 !important; padding: 8px 12px !important; font-size: 12px !important; line-height: 1.5 !important; }
|
||
.p-tooltip-text { background: transparent !important; border: none !important; padding: 0 !important; color: #e4e4e7 !important; white-space: normal !important; }
|
||
.p-tooltip-arrow { display: none !important; }
|
||
|
||
/* Context menu entries — clean */
|
||
.litecontextmenu .litemenu-entry, .litecontextmenu .litemenu-entry.submenu { background: transparent !important; border: none !important; outline: none !important; color: #a1a1aa !important; }
|
||
.litecontextmenu .litemenu-entry:hover:not(.separator) { background: rgba(244,244,245,0.06) !important; color: #e4e4e7 !important; }
|
||
.litecontextmenu, .litegraph.litecontextmenu { background: #131313 !important; border: none !important; border-radius: 8px !important; box-shadow: none !important; }
|
||
|
||
/* Context menu submenu arrows */
|
||
.litecontextmenu .litemenu-entry.has_submenu { display: flex !important; justify-content: space-between !important; align-items: center !important; border-right: none !important; }
|
||
.litecontextmenu .litemenu-entry.has_submenu::after { content: '›' !important; color: #48484f !important; font-size: 16px !important; margin-left: 12px !important; }
|
||
|
||
/* Main Menu */
|
||
.comfy-command-menu, .p-tieredmenu { background: #131313 !important; border: none !important; border-radius: 10px !important; box-shadow: none !important; }
|
||
.comfy-command-menu[class*="bg-"], .p-tieredmenu[class*="bg-"] { background: #131313 !important; box-shadow: none !important; }
|
||
.p-tieredmenu-root-list, .comfy-command-menu .p-tieredmenu-root-list, .comfy-command-menu > ul, .p-tieredmenu > ul { background: #131313 !important; }
|
||
.comfy-command-menu .p-menuitem-content, .p-tieredmenu .p-menuitem-content { background: transparent !important; border: none !important; color: #e4e4e7 !important; border-radius: 6px !important; }
|
||
.comfy-command-menu .p-menuitem-content:hover, .comfy-command-menu .p-menuitem:hover > .p-menuitem-content { background: rgba(244,244,245,0.06) !important; }
|
||
.comfy-command-menu .p-menuitem-text, .comfy-command-menu span, .comfy-command-menu label, .p-tieredmenu .p-menuitem-text { color: #e4e4e7 !important; }
|
||
.comfy-command-menu svg, .comfy-command-menu i, .comfy-command-menu [class*="icon"], .p-tieredmenu .p-menuitem-icon { color: #e4e4e7 !important; }
|
||
.comfy-command-menu .p-submenu-icon { color: #48484f !important; }
|
||
.comfy-command-menu .p-menuitem-separator, .p-tieredmenu .p-menuitem-separator { border-color: #1e1e22 !important; background: transparent !important; margin: 4px 8px !important; }
|
||
.comfy-command-menu .p-badge, .comfy-command-menu .p-tag { background: rgba(244,244,245,0.12) !important; color: #a1a1aa !important; border: none !important; border-radius: 4px !important; }
|
||
.comfy-command-menu .p-submenu-header { background: transparent !important; color: #48484f !important; border: none !important; }
|
||
.p-tieredmenu .p-tieredmenu-submenu, .comfy-command-menu .p-tieredmenu-submenu { background: #131313 !important; border: none !important; border-radius: 10px !important; margin-left: 14px !important; box-shadow: none !important; }
|
||
|
||
/* Manager Dialog */
|
||
.global-dialog.p-dialog { background: #131313 !important; border: none !important; border-radius: 16px !important; }
|
||
.global-dialog .p-dialog-header { background: #131313 !important; border-bottom: 1px solid #1a1a1a !important; }
|
||
.global-dialog .p-dialog-content { background: #131313 !important; }
|
||
.global-dialog .p-dialog-title { color: #e4e4e7 !important; }
|
||
.global-dialog button:not(.p-dialog-header-close) { background: #1a1a1a !important; color: #e4e4e7 !important; }
|
||
.global-dialog button:not(.p-dialog-header-close):hover { background: #222226 !important; color: #ffffff !important; }
|
||
.global-dialog select { background: #1a1a1a !important; color: #e4e4e7 !important; color-scheme: dark !important; }
|
||
|
||
/* Manager cm classes — no borders until hover */
|
||
.cm-menu-combo, .cm-button, .cm-button-red, .cm-button-orange, .cm-experimental-button, select.cm-menu-combo { border: 1px solid transparent !important; }
|
||
.cm-menu-combo:hover, .cm-button:hover, .cm-button-red:hover, .cm-button-orange:hover, select.cm-menu-combo:hover { border: 1px solid #2a2a2e !important; }
|
||
fieldset.cm-experimental, [class*="experimental"] { border: none !important; background: transparent !important; }
|
||
|
||
/* Notice board */
|
||
.cm-notice-board { background: #1a1a1a !important; border: 1px solid #1a1a1a !important; border-radius: 10px !important; padding: 24px 28px !important; margin: 16px 8px !important; max-height: 200px !important; overflow-y: auto !important; color: #a1a1aa !important; }
|
||
.cm-notice-board b, .cm-notice-board strong { color: #e4e4e7 !important; }
|
||
.cm-notice-board a { color: #8e8e99 !important; }
|
||
.cm-notice-board hr { border: none !important; border-top: 1px solid #1a1a1a !important; }
|
||
|
||
/* Arrow buttons in Manager */
|
||
[id*="cm-manual-button-arrow"] { background: transparent !important; border: none !important; }
|
||
|
||
/* Slider handle centered */
|
||
.p-slider { position: relative !important; height: 4px !important; }
|
||
.p-slider .p-slider-handle { position: absolute !important; top: 50% !important; transform: translateY(-50%) !important; margin-top: 0 !important; width: 14px !important; height: 14px !important; border-radius: 50% !important; background: #e4e4e7 !important; border: 2px solid #e4e4e7 !important; font-size: 0 !important; text-indent: -9999px !important; overflow: hidden !important; }
|
||
.p-slider .p-slider-handle::before, .p-slider .p-slider-handle::after { display: none !important; }
|
||
|
||
/* Env button — no grey bg on icon */
|
||
.actionbar-container .mdi { background: transparent !important; }
|
||
|
||
/* Run button — white icon/text */
|
||
.actionbar .p-panel-content button[class*="bg-primary"] { color: #ffffff !important; }
|
||
.actionbar .p-panel-content button[class*="bg-primary"] svg { color: #ffffff !important; }
|
||
|
||
/* Drag handle — 6 dot grip */
|
||
.drag-handle { opacity: 0.6 !important; color: #52525b !important; }
|
||
.drag-handle:hover { opacity: 1 !important; color: #a1a1aa !important; }
|
||
|
||
/* Workflow badge in sidebar */
|
||
.side-tool-bar-container .p-badge { background: #e4e4e7 !important; color: #0d0d0d !important; font-size: 10px !important; font-weight: 600 !important; min-width: 16px !important; height: 16px !important; border-radius: 8px !important; }
|
||
|
||
/* Run dropdown */
|
||
.z-1000[class*="rounded-lg"][class*="border"], .border-border-subtle { background: #131313 !important; border-color: transparent !important; }
|
||
|
||
/* Workflow badge — extra selectors */
|
||
.side-tool-bar-container [class*="badge"], .side-tool-bar-container sup { background: #e4e4e7 !important; color: #0d0d0d !important; font-size: 10px !important; font-weight: 600 !important; border-radius: 8px !important; }
|
||
|
||
/* Node value editor (input popup on canvas) */
|
||
.graphdialog, .graphdialog input, .graphdialog select { background: #131313 !important; border: 1px solid #1e1e22 !important; border-radius: 8px !important; color: #e4e4e7 !important; }
|
||
.graphdialog button { background: #1a1a1a !important; border: none !important; color: #e4e4e7 !important; border-radius: 6px !important; }
|
||
.graphdialog button:hover { background: #222226 !important; }
|
||
|
||
/* LiteGraph value dropdown (filter list, combobox) */
|
||
.litecontextmenu .litemenu-entry.selected,
|
||
.litecontextmenu .litemenu-entry:focus,
|
||
.litecontextmenu .litemenu-entry[class*="selected"] { background: rgba(244,244,245,0.08) !important; color: #e4e4e7 !important; }
|
||
|
||
/* Node widget edit input */
|
||
.litesearchbox, .litesearchbox input { background: #131313 !important; color: #e4e4e7 !important; border-color: #1e1e22 !important; }
|
||
|
||
/* Generic dialog/popup value editing */
|
||
.graphdialog { color-scheme: dark !important; }
|
||
|
||
/* Widget combo dropdown */
|
||
.litecontextmenu input { background: #1a1a1a !important; border: 1px solid #1e1e22 !important; border-radius: 6px !important; color: #e4e4e7 !important; }
|
||
|
||
/* Highlighted/selected option in any dropdown — white not blue */
|
||
::highlight, ::selection { background: rgba(244,244,245,0.15) !important; color: #ffffff !important; }
|
||
`;
|
||
|
||
// CSS-only protection active from this point, extension API or not
|
||
injectLinearThemeCSS();
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
Extension Registration
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
|
||
// Workflow colors belong to the workflow; the theme only changes defaults.
|
||
|
||
// Ensure node is wide enough to show full title without truncation
|
||
|
||
/* ── Arrange selected nodes: straight main chain, sub-nodes below target ── */
|
||
function arrangeSelectedNodes(selectedNodes) {
|
||
if (selectedNodes.length === 0) return;
|
||
|
||
const graph = comfyApp.graph;
|
||
const GRID = 10;
|
||
const H_GAP = 40; // 4 dots horizontal
|
||
const V_GAP = 10; // 1 dot vertical
|
||
|
||
const nodeSet = new Set(selectedNodes.map(n => n.id));
|
||
const nodeById = {};
|
||
for (const n of selectedNodes) nodeById[n.id] = n;
|
||
|
||
// --- 1. Topological depth (column index) ---
|
||
const depth = {};
|
||
const computing = new Set();
|
||
|
||
function getDepth(node) {
|
||
if (depth[node.id] !== undefined) return depth[node.id];
|
||
if (computing.has(node.id)) return 0;
|
||
computing.add(node.id);
|
||
|
||
let maxParentDepth = -1;
|
||
if (node.inputs) {
|
||
for (const input of node.inputs) {
|
||
if (input.link != null) {
|
||
const linkInfo = graph.links[input.link];
|
||
if (linkInfo && nodeSet.has(linkInfo.origin_id)) {
|
||
const pd = getDepth(nodeById[linkInfo.origin_id]);
|
||
if (pd > maxParentDepth) maxParentDepth = pd;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
depth[node.id] = maxParentDepth + 1;
|
||
computing.delete(node.id);
|
||
return depth[node.id];
|
||
}
|
||
|
||
for (const node of selectedNodes) getDepth(node);
|
||
|
||
// --- 1b. Find main path early (needed for depth reassignment) ---
|
||
function findMainPathEarly() {
|
||
const roots = selectedNodes.filter(n => depth[n.id] === 0);
|
||
let bestPath = [];
|
||
|
||
function dfs(node, path) {
|
||
path.push(node);
|
||
let extended = false;
|
||
if (node.outputs) {
|
||
for (const out of node.outputs) {
|
||
if (out.links) {
|
||
for (const linkId of out.links) {
|
||
const linkInfo = graph.links[linkId];
|
||
if (linkInfo && nodeSet.has(linkInfo.target_id)) {
|
||
dfs(nodeById[linkInfo.target_id], path);
|
||
extended = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!extended && path.length > bestPath.length) {
|
||
bestPath = [...path];
|
||
}
|
||
path.pop();
|
||
}
|
||
|
||
for (const root of roots) dfs(root, []);
|
||
return new Set(bestPath.map(n => n.id));
|
||
}
|
||
|
||
const mainPathIds = findMainPathEarly();
|
||
|
||
// --- 1c. Reassign sub-node depths: place at min(target_depth) - 1 ---
|
||
// Places sub-nodes next to their EARLIEST target, not the latest
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
for (const node of selectedNodes) {
|
||
if (mainPathIds.has(node.id)) continue;
|
||
if (!node.outputs) continue;
|
||
|
||
// Find the MINIMUM target depth in selection
|
||
let minTargetDepth = Infinity;
|
||
for (const out of node.outputs) {
|
||
if (out.links) {
|
||
for (const linkId of out.links) {
|
||
const linkInfo = graph.links[linkId];
|
||
if (linkInfo && nodeSet.has(linkInfo.target_id)) {
|
||
if (depth[linkInfo.target_id] < minTargetDepth) {
|
||
minTargetDepth = depth[linkInfo.target_id];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (minTargetDepth !== Infinity) {
|
||
const newDepth = minTargetDepth - 1;
|
||
if (newDepth >= 0 && newDepth > depth[node.id]) {
|
||
depth[node.id] = newDepth;
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 2. Group by depth ---
|
||
const columns = {};
|
||
for (const node of selectedNodes) {
|
||
const d = depth[node.id];
|
||
if (!columns[d]) columns[d] = [];
|
||
columns[d].push(node);
|
||
}
|
||
|
||
// --- 3. Helpers ---
|
||
function getNodeSize(node) {
|
||
if (node.flags.collapsed) {
|
||
return [
|
||
node._collapsed_width || node.size[0],
|
||
LiteGraph.NODE_TITLE_HEIGHT || 30
|
||
];
|
||
}
|
||
return [node.size[0], node.size[1]];
|
||
}
|
||
|
||
function findTarget(node) {
|
||
if (!node.outputs) return null;
|
||
let best = null;
|
||
let bestDepthDiff = Infinity;
|
||
|
||
for (const out of node.outputs) {
|
||
if (out.links) {
|
||
for (const linkId of out.links) {
|
||
const linkInfo = graph.links[linkId];
|
||
if (linkInfo && nodeSet.has(linkInfo.target_id)) {
|
||
const target = nodeById[linkInfo.target_id];
|
||
const dd = depth[target.id] - depth[node.id];
|
||
if (dd < bestDepthDiff || (dd === bestDepthDiff && (!best || linkInfo.target_slot < best.slot))) {
|
||
best = { node: target, slot: linkInfo.target_slot };
|
||
bestDepthDiff = dd;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
const snap = (v) => Math.round(v / GRID) * GRID;
|
||
const startX = Math.min(...selectedNodes.map(n => n.pos[0]));
|
||
const startY = Math.min(...selectedNodes.map(n => n.pos[1]));
|
||
const mainY = snap(startY);
|
||
const depths = Object.keys(columns).map(Number).sort((a, b) => a - b);
|
||
|
||
// --- 5. Compute X positions (left → right) ---
|
||
const columnX = {};
|
||
let curX = snap(startX);
|
||
for (const d of depths) {
|
||
columnX[d] = curX;
|
||
let maxW = 0;
|
||
for (const node of columns[d]) {
|
||
const [w] = getNodeSize(node);
|
||
if (w > maxW) maxW = w;
|
||
}
|
||
curX = snap(curX + maxW + H_GAP);
|
||
}
|
||
|
||
// --- 6. Place ALL nodes per column, sorted by target slot (slot 0 = top) ---
|
||
for (const d of depths) {
|
||
const col = columns[d];
|
||
|
||
// Sort all nodes by the slot they connect to on their target
|
||
col.sort((a, b) => {
|
||
const ta = findTarget(a);
|
||
const tb = findTarget(b);
|
||
const sa = ta ? ta.slot : -1; // no target = top (roots)
|
||
const sb = tb ? tb.slot : -1;
|
||
return sa - sb;
|
||
});
|
||
|
||
// Stack from mainY
|
||
let curY = snap(mainY);
|
||
for (const node of col) {
|
||
const [, h] = getNodeSize(node);
|
||
node.pos[0] = columnX[d];
|
||
node.pos[1] = curY;
|
||
curY = snap(curY + h + V_GAP);
|
||
}
|
||
}
|
||
|
||
comfyApp.canvas.setDirty(true, true);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
Module-scope dot grid data URL (shared with ThemeEditor)
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
let currentDotDataUrl = null;
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
Execution Glow — State Tracking
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
const executionState = {
|
||
runningNodeId: null,
|
||
completedNodes: {}, // nodeId → timestamp
|
||
errorNodes: {}, // nodeId → timestamp
|
||
};
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
Theme Editor — Floating Panel
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
|
||
const THEME_EDITOR_CSS = `
|
||
.te-panel { position: fixed; top: 60px; right: 20px; width: 320px; max-height: min(720px, calc(100vh - 80px)); overflow-y: auto; background: #131313; border: none; border-radius: 12px; z-index: 99999; font-family: Inter, system-ui, sans-serif; font-size: 12px; color: #a1a1aa; box-shadow: none; display: none; max-width: calc(100vw - 16px); box-sizing: border-box; color-scheme: dark; --te-text: #e4e4e7; --te-secondary: #a1a1aa; --te-border: #27272a; }
|
||
.te-panel.open { display: block; }
|
||
.te-panel * { box-sizing: border-box; }
|
||
.te-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; cursor: grab; user-select: none; border-bottom: 1px solid #1e1e22; position: sticky; top: 0; z-index: 1; background: #111113; gap: 8px; }
|
||
.te-header:active { cursor: grabbing; }
|
||
.te-title { font-weight: 600; color: #e4e4e7; font-size: 13px; flex: 1; }
|
||
.te-header-btns { display: flex; gap: 4px; }
|
||
.te-btn { background: none; border: none; color: var(--te-secondary); cursor: pointer; font-size: 14px; padding: 3px; border-radius: 4px; line-height: 1; min-width: 24px; min-height: 24px; display: inline-flex; align-items: center; justify-content: center; }
|
||
.te-btn:hover { color: #f4f4f5; background: rgba(244,244,245,0.06); }
|
||
.te-section { border-bottom: 1px solid #1a1a1a; }
|
||
.te-section:last-child { border-bottom: none; }
|
||
.te-section-header { display: flex; align-items: center; padding: 9px 12px; cursor: pointer; user-select: none; font-weight: 600; font-size: 11px; letter-spacing: .03em; color: #e4e4e7; text-transform: uppercase; }
|
||
.te-section-header:hover { color: #ffffff; }
|
||
.te-arrow { margin-right: 6px; font-size: 8px; display: inline-block; }
|
||
.te-section.open .te-arrow { transform: rotate(90deg); }
|
||
.te-section-body { display: none; padding: 4px 12px 10px; }
|
||
.te-section.open .te-section-body { display: block; }
|
||
.te-row { display: flex; align-items: center; justify-content: space-between; padding: 2px 0; gap: 8px; min-height: 28px; }
|
||
.te-label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; color: #a1a1aa; min-width: 0; line-height: 1.35; }
|
||
.te-color { -webkit-appearance: none; appearance: none; width: 28px; height: 24px; border: none; border-radius: 6px; cursor: pointer; background: none; padding: 0; }
|
||
.te-color::-webkit-color-swatch-wrapper { padding: 0; }
|
||
.te-color::-webkit-color-swatch { border: none; border-radius: 3px; }
|
||
.te-color::-moz-color-swatch { border: none; border-radius: 3px; }
|
||
.te-range { width: 88px; height: 24px; -webkit-appearance: none; appearance: none; background: transparent; border-radius: 2px; outline: none; cursor: pointer; flex-shrink: 0; }
|
||
.te-range::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; background: #f4f4f5; border-radius: 50%; cursor: pointer; margin-top: -4px; }
|
||
.te-range::-moz-range-thumb { width: 12px; height: 12px; background: #f4f4f5; border-radius: 50%; cursor: pointer; border: none; }
|
||
.te-range-val { width: 32px; text-align: right; font-size: 11px; color: #a1a1aa; font-variant-numeric: tabular-nums; }
|
||
.te-field-reset { font-size: 11px; padding: 1px 3px; opacity: 0; pointer-events: none; flex-shrink: 0; flex: 0 0 24px; }
|
||
.te-row.modified .te-field-reset { opacity: 1; pointer-events: auto; }
|
||
.te-presets { border-bottom: 1px solid #1a1a1a; padding: 8px 12px; }
|
||
.te-presets-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
|
||
.te-presets-bar span { font-weight: 600; font-size: 11px; letter-spacing: 0.06em; color: #e4e4e7; text-transform: uppercase; }
|
||
.te-preset-save { font-size: 11px; padding: 3px 8px; border-radius: 6px; background: #1a1a1a; border: none; color: #e4e4e7; cursor: pointer; min-height: 24px; }
|
||
.te-preset-save:hover { color: #ffffff; background: #222226; }
|
||
.te-preset-list { display: flex; flex-direction: column; gap: 2px; }
|
||
.te-preset-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px; border-radius: 4px; cursor: pointer; font-size: 11px; color: #a1a1aa; }
|
||
.te-preset-item:hover { background: rgba(244,244,245,0.06); color: #f4f4f5; }
|
||
.te-preset-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: none; background: transparent; text-align: left; cursor: pointer; padding: 3px 0; color: inherit; font-size: 11px; }
|
||
.te-preset-del { font-size: 10px; padding: 1px 3px; opacity: 0; }
|
||
.te-preset-empty { font-size: 12px; color: var(--te-secondary); padding: 4px 0; }
|
||
.te-panel::-webkit-scrollbar { width: 4px; }
|
||
.te-panel::-webkit-scrollbar-track { background: transparent; }
|
||
.te-panel::-webkit-scrollbar-thumb { background: #27272a; border-radius: 2px; }
|
||
.te-save-status { font-size: 10px; color: #a1a1aa; }
|
||
.te-save-status.error { color: #f87171; }
|
||
.te-panel input, .te-panel button, .te-preset-item-name { font-family: 'Inter', Arial, sans-serif !important; }
|
||
.te-panel input[type="checkbox"] { accent-color: #a1a1aa; width: 14px; height: 14px; }
|
||
.te-panel button { box-shadow: none; }
|
||
.te-preset-item:hover .te-preset-del, .te-preset-item:focus-within .te-preset-del { opacity: 1; }
|
||
.te-panel :is(button, input, [role="button"]):focus-visible { outline: 2px solid #a1a1aa !important; outline-offset: 2px; scroll-margin-top: 52px; scroll-margin-bottom: 8px; }
|
||
.actionbar-container button:hover, .actionbar button:hover { background-color: rgba(244,244,245,0.08) !important; }
|
||
input:not([type]), button:not([class*="icon"]), select { font-family: 'Inter', Arial, sans-serif !important; }
|
||
.lg-node { --radius-xl: var(--linear-node-radius, 11px); filter: var(--linear-node-shadow, none) !important; }
|
||
.lg-node [data-testid="node-title"] { color: var(--linear-node-title-text, #ffffff); }
|
||
.lg-node textarea, .lg-node input:not([type="checkbox"]):not([type="range"]), textarea.comfy-multiline-input { background: var(--linear-node-widget-bg, #1a1a1a) !important; }
|
||
.te-range::-webkit-slider-runnable-track { height: 4px; background: #27272a; border-radius: 2px; }
|
||
.te-range::-moz-range-track { height: 4px; background: #27272a; border-radius: 2px; }
|
||
.comfyui-button.primary:not(:disabled) { color: var(--linear-text-secondary, #a1a1aa) !important; }
|
||
.comfyui-button.primary:not(:disabled):hover { color: var(--linear-text-primary, #e4e4e7) !important; }
|
||
.te-section-header { width: 100%; border: none; border-radius: 0; background: transparent; text-align: left; font-family: inherit; }
|
||
.te-field-reset:disabled { opacity: 0; pointer-events: none; }
|
||
.te-row:focus-within .te-field-reset:not(:disabled) { opacity: 1; pointer-events: auto; }
|
||
.te-preset-form { display: grid; grid-template-columns: 1fr auto auto; gap: 6px; margin: 6px 0 8px; }
|
||
.te-preset-form[hidden], .te-save-retry[hidden] { display: none; }
|
||
.te-preset-form label { grid-column: 1 / -1; font-size: 12px; color: #a1a1aa; }
|
||
.te-preset-form input { min-width: 0; width: 100%; height: 28px; padding: 4px 6px; font-size: 12px !important; }
|
||
.te-preset-form button, .te-save-retry { min-height: 24px; border: none; background: #1a1a1a; color: #e4e4e7; border-radius: 5px; padding: 3px 6px; cursor: pointer; font-size: 11px; }
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.te-panel *, .lg-node * { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||
}
|
||
|
||
`;
|
||
|
||
const THEME_DEFAULTS = {
|
||
canvas: {
|
||
backgroundColor: "#0d0d0d",
|
||
dotGridOpacity: 0,
|
||
nodeBorderRadius: 11,
|
||
connectionWidth: 1.5,
|
||
linkColor: "#52525b",
|
||
groupColor: "#2a2a2e",
|
||
fpsBoost: true,
|
||
snapGuides: true,
|
||
},
|
||
nodes: {
|
||
bodyColor: "#1a1a1a",
|
||
titleColor: "#1a1a1a",
|
||
shadowOpacity: 0,
|
||
boxIndicatorColor: "#ffffff",
|
||
outlineColor: "#ffffff",
|
||
outlineTransparent: true,
|
||
titleTextColor: "#ffffff",
|
||
widgetBackground: "#1a1a1a",
|
||
},
|
||
slots: {
|
||
CLIP: "#FACC15", CONDITIONING: "#FB923C", IMAGE: "#60A5FA",
|
||
LATENT: "#F0ABFC", MASK: "#4ADE80", MODEL: "#A78BFA",
|
||
VAE: "#F87171", CONTROL_NET: "#34D399",
|
||
},
|
||
ui: {
|
||
barsBackground: "#1a1a1a",
|
||
surface: "#1a1a1a", surfaceElevated: "#121212",
|
||
border: "#1e1e22", textPrimary: "#ffffff",
|
||
textSecondary: "#b0b0b0", textMuted: "#71717a",
|
||
accent: "#ffffff", accentSecondary: "#ffffff",
|
||
},
|
||
};
|
||
|
||
const UI_VAR_MAP = {
|
||
surface: [
|
||
"--linear-surface", "--interface-panel-surface", "--interface-panel-header",
|
||
"--p-content-background", "--p-overlay-background", "--dialog-surface",
|
||
"--modal-card-background", "--p-dialog-background", "--p-surface-100",
|
||
],
|
||
surfaceElevated: [
|
||
"--linear-surface-elevated", "--p-surface-200",
|
||
"--button-surface", "--p-form-field-background", "--p-inputtext-background",
|
||
"--p-select-background", "--p-button-background", "--p-button-primary-background",
|
||
"--node-component-surface",
|
||
],
|
||
border: [
|
||
"--linear-border", "--border-color", "--interface-stroke",
|
||
"--interface-panel-border", "--p-content-border-color",
|
||
"--p-overlay-border-color", "--dialog-border", "--modal-card-border",
|
||
"--p-dialog-border-color", "--p-tabs-tablist-border-color",
|
||
],
|
||
textPrimary: [
|
||
"--linear-text-primary", "--fg-color", "--text-primary", "--color-text-primary",
|
||
"--base-foreground", "--p-text-color", "--p-content-color",
|
||
"--interface-menu-text", "--button-text", "--p-form-field-color",
|
||
"--p-inputtext-color", "--p-select-color",
|
||
"--p-datatable-row-color", "--p-toast-color", "--p-dialog-color",
|
||
],
|
||
textSecondary: [
|
||
"--linear-text-secondary", "--text-secondary", "--color-text-secondary",
|
||
"--drag-text", "--p-text-secondary-color", "--interface-menu-text-secondary",
|
||
"--button-text-secondary",
|
||
],
|
||
textMuted: [
|
||
"--linear-text-muted", "--text-muted", "--descrip-text",
|
||
"--p-text-muted-color", "--p-tabs-tab-color",
|
||
],
|
||
accent: [
|
||
"--linear-accent", "--p-primary-color",
|
||
],
|
||
accentSecondary: [
|
||
"--linear-accent-secondary", "--input-border-focus",
|
||
"--p-form-field-focus-border-color", "--p-select-focus-border-color",
|
||
"--p-inputtext-focus-border-color", "--p-focus-ring-color",
|
||
],
|
||
};
|
||
|
||
const SECTION_DEFS = [
|
||
{
|
||
key: "canvas", label: "Canvas",
|
||
fields: [
|
||
{ key: "backgroundColor", label: "Background", type: "color" },
|
||
{ key: "dotGridOpacity", label: "Dot Grid", type: "range", min: 0, max: 0.5, step: 0.01 },
|
||
{ key: "nodeBorderRadius", label: "Node Radius", type: "range", min: 0, max: 20, step: 1 },
|
||
{ key: "connectionWidth", label: "Connection Width", type: "range", min: 1, max: 6, step: 0.5 },
|
||
{ key: "linkColor", label: "Link Color", type: "color" },
|
||
{ key: "groupColor", label: "Default Group Tint", type: "color" },
|
||
{ key: "fpsBoost", label: "FPS Boost", type: "checkbox" },
|
||
{ key: "snapGuides", label: "Snap Guides", type: "checkbox" },
|
||
],
|
||
},
|
||
{
|
||
key: "nodes", label: "Nodes",
|
||
fields: [
|
||
{ key: "bodyColor", label: "Body Color", type: "color" },
|
||
{ key: "titleColor", label: "Title Color", type: "color" },
|
||
{ key: "shadowOpacity", label: "Shadow", type: "range", min: 0, max: 1, step: 0.05 },
|
||
{ key: "boxIndicatorColor", label: "Box Indicator", type: "color" },
|
||
{ key: "outlineColor", label: "Outline", type: "color" },
|
||
{ key: "outlineTransparent", label: "Outline Transparent", type: "checkbox" },
|
||
{ key: "titleTextColor", label: "Title Text", type: "color" },
|
||
{ key: "widgetBackground", label: "Widget BG", type: "color" },
|
||
],
|
||
},
|
||
{
|
||
key: "slots", label: "Slots",
|
||
fields: Object.keys(THEME_DEFAULTS.slots).map(k => ({ key: k, label: k, type: "color" })),
|
||
},
|
||
{
|
||
key: "ui", label: "UI",
|
||
fields: [
|
||
{ key: "barsBackground", label: "Bars Color", type: "color" },
|
||
{ key: "surface", label: "Surface", type: "color" },
|
||
{ key: "surfaceElevated", label: "Surface Elevated", type: "color" },
|
||
{ key: "border", label: "Border", type: "color" },
|
||
{ key: "textPrimary", label: "Text Primary", type: "color" },
|
||
{ key: "textSecondary", label: "Text Secondary", type: "color" },
|
||
{ key: "textMuted", label: "Text Muted", type: "color" },
|
||
{ key: "accent", label: "Accent", type: "color" },
|
||
{ key: "accentSecondary", label: "Accent Secondary", type: "color" },
|
||
],
|
||
},
|
||
];
|
||
|
||
function normalizeThemeConfig(saved) {
|
||
const config = JSON.parse(JSON.stringify(THEME_DEFAULTS));
|
||
for (const section of SECTION_DEFS) {
|
||
for (const field of section.fields) {
|
||
const value = saved?.[section.key]?.[field.key];
|
||
if (field.type === "color" && typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value)) {
|
||
config[section.key][field.key] = value;
|
||
} else if (field.type === "checkbox" && typeof value === "boolean") {
|
||
config[section.key][field.key] = value;
|
||
} else if (field.type === "range" && typeof value === "number" && Number.isFinite(value)) {
|
||
config[section.key][field.key] = Math.min(field.max, Math.max(field.min, value));
|
||
}
|
||
}
|
||
}
|
||
return config;
|
||
}
|
||
|
||
class ThemeEditor {
|
||
constructor() {
|
||
this.config = JSON.parse(JSON.stringify(THEME_DEFAULTS));
|
||
this.panel = null;
|
||
this._styleEl = null;
|
||
this._pendingSections = new Set();
|
||
this._applyFrame = null;
|
||
this._returnFocus = null;
|
||
this._pendingSave = null;
|
||
this._savePromise = null;
|
||
}
|
||
|
||
async init() {
|
||
this._injectCSS();
|
||
this._createPanel();
|
||
await this._loadConfig();
|
||
this._syncInputsToConfig();
|
||
this.applyConfig(false);
|
||
this._renderPresets();
|
||
}
|
||
|
||
_injectCSS() {
|
||
if (this._styleEl) return;
|
||
this._styleEl = document.createElement("style");
|
||
this._styleEl.textContent = THEME_EDITOR_CSS;
|
||
document.head.appendChild(this._styleEl);
|
||
}
|
||
|
||
_createPanel() {
|
||
const panel = document.createElement("div");
|
||
panel.className = "te-panel";
|
||
panel.innerHTML = `
|
||
<div class="te-header">
|
||
<span class="te-title">Theme Editor</span>
|
||
<span class="te-save-status" role="status" aria-live="polite"></span>
|
||
<button type="button" class="te-save-retry" hidden>Retry</button>
|
||
<div class="te-header-btns">
|
||
<button class="te-btn te-reset" title="Reset to defaults">↺</button>
|
||
<button class="te-btn te-close" title="Close">✕</button>
|
||
</div>
|
||
</div>
|
||
<div class="te-body"></div>
|
||
`;
|
||
|
||
const body = panel.querySelector(".te-body");
|
||
|
||
// Presets section
|
||
const presetsDiv = document.createElement("div");
|
||
presetsDiv.className = "te-presets";
|
||
presetsDiv.innerHTML = `
|
||
<div class="te-presets-bar">
|
||
<span>Presets</span>
|
||
<button class="te-preset-save">Save</button>
|
||
</div>
|
||
<form class="te-preset-form" hidden>
|
||
<label for="te-preset-name">Preset name</label>
|
||
<input id="te-preset-name" type="text" autocomplete="off" required>
|
||
<button type="submit">Save</button>
|
||
<button type="button" class="te-preset-cancel">Cancel</button>
|
||
</form>
|
||
<div class="te-preset-list"></div>
|
||
`;
|
||
body.appendChild(presetsDiv);
|
||
this._presetListEl = presetsDiv.querySelector(".te-preset-list");
|
||
|
||
const presetForm = presetsDiv.querySelector(".te-preset-form");
|
||
const presetName = presetForm.querySelector("input");
|
||
const savePresetButton = presetsDiv.querySelector(".te-preset-save");
|
||
const closePresetForm = () => {
|
||
presetForm.hidden = true;
|
||
presetName.value = "";
|
||
savePresetButton.focus({ preventScroll: true });
|
||
};
|
||
savePresetButton.addEventListener("click", () => {
|
||
presetForm.hidden = false;
|
||
presetName.focus({ preventScroll: true });
|
||
});
|
||
presetForm.addEventListener("submit", e => {
|
||
e.preventDefault();
|
||
const name = presetName.value.trim();
|
||
if (!name) { presetName.focus(); return; }
|
||
this._savePreset(name);
|
||
closePresetForm();
|
||
});
|
||
presetForm.querySelector(".te-preset-cancel").addEventListener("click", closePresetForm);
|
||
presetForm.addEventListener("keydown", e => {
|
||
if (e.key === "Escape") {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
closePresetForm();
|
||
}
|
||
});
|
||
|
||
const savedSections = this._loadSections();
|
||
|
||
for (const sec of SECTION_DEFS) {
|
||
const section = document.createElement("div");
|
||
section.className = "te-section" + (savedSections[sec.key] ? " open" : "");
|
||
section.dataset.section = sec.key;
|
||
|
||
let fieldsHtml = "";
|
||
for (const f of sec.fields) {
|
||
const val = this.config[sec.key][f.key];
|
||
const rb = `<button class="te-btn te-field-reset" data-section="${sec.key}" data-key="${f.key}" title="Reset ${f.label}" aria-label="Reset ${f.label}">↺</button>`;
|
||
if (f.type === "color") {
|
||
fieldsHtml += `<div class="te-row">
|
||
<label class="te-label" for="te-${sec.key}-${f.key}" title="${f.label}">${f.label}</label>
|
||
<input type="color" class="te-color" data-section="${sec.key}" data-key="${f.key}" id="te-${sec.key}-${f.key}" aria-label="${f.label}" value="${val}">
|
||
${rb}
|
||
</div>`;
|
||
} else if (f.type === "range") {
|
||
fieldsHtml += `<div class="te-row">
|
||
<label class="te-label" for="te-${sec.key}-${f.key}" title="${f.label}">${f.label}</label>
|
||
<input type="range" class="te-range" data-section="${sec.key}" data-key="${f.key}" id="te-${sec.key}-${f.key}" aria-label="${f.label}"
|
||
min="${f.min}" max="${f.max}" step="${f.step}" value="${val}">
|
||
<span class="te-range-val">${val}</span>
|
||
${rb}
|
||
</div>`;
|
||
} else if (f.type === "checkbox") {
|
||
fieldsHtml += `<div class="te-row">
|
||
<label class="te-label" for="te-${sec.key}-${f.key}" title="${f.label}">${f.label}</label>
|
||
<input type="checkbox" data-section="${sec.key}" data-key="${f.key}" id="te-${sec.key}-${f.key}" aria-label="${f.label}"
|
||
${val ? "checked" : ""} style="cursor:pointer;">
|
||
${rb}
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
section.innerHTML = `
|
||
<button type="button" class="te-section-header" aria-expanded="${!!savedSections[sec.key]}" aria-controls="te-section-${sec.key}"><span class="te-arrow" aria-hidden="true">▶</span>${sec.label}</button>
|
||
<div class="te-section-body" id="te-section-${sec.key}">${fieldsHtml}</div>
|
||
`;
|
||
body.appendChild(section);
|
||
}
|
||
|
||
// Stop propagation so canvas doesn't react
|
||
for (const evt of ["pointerdown", "pointermove", "pointerup", "wheel", "keydown", "keyup"]) {
|
||
panel.addEventListener(evt, e => e.stopPropagation());
|
||
}
|
||
|
||
panel.querySelector(".te-close").addEventListener("click", () => this.toggle(false));
|
||
panel.querySelector(".te-reset").addEventListener("click", () => this.resetConfig());
|
||
panel.querySelector(".te-save-retry").addEventListener("click", () => this.saveConfig());
|
||
|
||
// Per-field reset buttons
|
||
panel.addEventListener("click", (e) => {
|
||
const btn = e.target.closest(".te-field-reset");
|
||
if (!btn) return;
|
||
const section = btn.dataset.section;
|
||
const key = btn.dataset.key;
|
||
if (!section || !key) return;
|
||
this.config[section][key] = THEME_DEFAULTS[section][key];
|
||
this._applySection(section);
|
||
this._syncInputsToConfig();
|
||
this._updateModifiedState();
|
||
});
|
||
|
||
panel.querySelectorAll(".te-section-header").forEach(header => {
|
||
const toggleSection = () => {
|
||
const open = header.parentElement.classList.toggle("open");
|
||
header.setAttribute("aria-expanded", String(open));
|
||
this._saveSections();
|
||
};
|
||
header.addEventListener("click", toggleSection);
|
||
|
||
});
|
||
|
||
panel.addEventListener("input", (e) => this._onInputChange(e));
|
||
panel.addEventListener("keydown", e => {
|
||
if (e.key === "Escape") this.toggle(false);
|
||
});
|
||
this._setupDrag(panel);
|
||
|
||
const pos = this._loadPosition();
|
||
if (pos) {
|
||
panel.style.top = pos.y + "px";
|
||
panel.style.left = pos.x + "px";
|
||
panel.style.right = "auto";
|
||
}
|
||
|
||
document.body.appendChild(panel);
|
||
this.panel = panel;
|
||
window.addEventListener("resize", () => this._clampPosition());
|
||
document.addEventListener("visibilitychange", () => {
|
||
if (document.hidden) this._flushSections();
|
||
});
|
||
}
|
||
|
||
toggle(force) {
|
||
if (!this.panel) return;
|
||
const show = force !== undefined ? force : !this.panel.classList.contains("open");
|
||
const wasOpen = this.panel.classList.contains("open");
|
||
if (show && !wasOpen) this._returnFocus = document.activeElement;
|
||
this.panel.classList.toggle("open", show);
|
||
if (show) {
|
||
this._clampPosition();
|
||
if (!wasOpen) this.panel.querySelector(".te-close").focus({ preventScroll: true });
|
||
} else if (wasOpen) {
|
||
const target = this._returnFocus?.isConnected && this._returnFocus !== document.body
|
||
? this._returnFocus : comfyApp.canvas?.canvas;
|
||
target?.focus({ preventScroll: true });
|
||
}
|
||
}
|
||
|
||
_clampPosition() {
|
||
if (!this.panel?.classList.contains("open")) return;
|
||
const rect = this.panel.getBoundingClientRect();
|
||
const x = Math.max(8, Math.min(rect.left, window.innerWidth - rect.width - 8));
|
||
const y = Math.max(8, Math.min(rect.top, window.innerHeight - rect.height - 8));
|
||
this.panel.style.left = `${x}px`;
|
||
this.panel.style.top = `${y}px`;
|
||
this.panel.style.right = "auto";
|
||
}
|
||
|
||
// ── Apply functions ──
|
||
|
||
applyConfig(persist = true) {
|
||
if (this._applyFrame !== null) cancelAnimationFrame(this._applyFrame);
|
||
this._applyFrame = null;
|
||
this._pendingSections.clear();
|
||
this.applyCanvas();
|
||
this.applyNodes();
|
||
this.applySlots();
|
||
this.applyUI();
|
||
if (persist) this.saveConfig();
|
||
if (comfyApp.canvas) comfyApp.canvas.setDirty(true, true);
|
||
}
|
||
|
||
applyCanvas() {
|
||
const c = this.config.canvas;
|
||
const canvas = comfyApp.canvas;
|
||
if (!canvas) return;
|
||
|
||
// Background color — target every possible source
|
||
if (window.LiteGraph) {
|
||
LiteGraph.CLEAR_BACKGROUND_COLOR = c.backgroundColor;
|
||
}
|
||
// Instance property (what LiteGraph actually reads during render)
|
||
canvas.clear_background_color = c.backgroundColor;
|
||
document.body.style.background = c.backgroundColor;
|
||
// Set --bg-color on both root AND body (ComfyUI palette sets its own --bg-color on body)
|
||
document.documentElement.style.setProperty("--bg-color", c.backgroundColor);
|
||
document.body.style.setProperty("--bg-color", c.backgroundColor);
|
||
document.documentElement.style.setProperty("--base-background", c.backgroundColor);
|
||
document.documentElement.style.setProperty("--p-surface-0", c.backgroundColor);
|
||
document.documentElement.style.setProperty("--p-surface-50", c.backgroundColor);
|
||
if (canvas.canvas) canvas.canvas.style.background = c.backgroundColor;
|
||
|
||
this._buildDotGrid(c.dotGridOpacity);
|
||
canvas.round_radius = c.nodeBorderRadius;
|
||
document.documentElement.style.setProperty("--linear-node-radius", `${c.nodeBorderRadius}px`);
|
||
document.documentElement.style.setProperty("--linear-node-shadow", c.fpsBoost || !this.config.nodes.shadowOpacity
|
||
? "none" : `drop-shadow(0 2px 4px rgba(0,0,0,${this.config.nodes.shadowOpacity}))`);
|
||
canvas.connections_width = c.connectionWidth;
|
||
|
||
// Link color
|
||
if (c.linkColor && window.LiteGraph) {
|
||
LiteGraph.LINK_COLOR = c.linkColor;
|
||
}
|
||
|
||
// Default group tint is read by the custom group renderer.
|
||
this._updateIdleRendering?.();
|
||
|
||
// FPS Boost
|
||
if (c.fpsBoost !== undefined && canvas) {
|
||
canvas.render_shadows = !c.fpsBoost;
|
||
canvas.highquality_render = !c.fpsBoost;
|
||
// Native arrows always off — we draw custom chevrons instead
|
||
canvas.render_connection_arrows = false;
|
||
canvas.use_gradients = !c.fpsBoost;
|
||
canvas.render_collapsed_slots = !c.fpsBoost;
|
||
canvas.always_render_background = !c.fpsBoost;
|
||
// links_render_mode intentionally untouched (user setting wins).
|
||
}
|
||
}
|
||
|
||
applyNodes() {
|
||
const n = this.config.nodes;
|
||
if (!window.LiteGraph) return;
|
||
|
||
LiteGraph.NODE_DEFAULT_BGCOLOR = n.bodyColor;
|
||
LiteGraph.NODE_DEFAULT_COLOR = n.titleColor;
|
||
LiteGraph.DEFAULT_SHADOW_COLOR = `rgba(0,0,0,${n.shadowOpacity})`;
|
||
LiteGraph.NODE_DEFAULT_BOXCOLOR = n.boxIndicatorColor;
|
||
LiteGraph.NODE_BOX_OUTLINE_COLOR = n.outlineTransparent ? "rgba(0,0,0,0)" : n.outlineColor;
|
||
LiteGraph.NODE_TITLE_COLOR = n.titleTextColor;
|
||
document.documentElement.style.setProperty("--component-node-title-color", n.titleTextColor);
|
||
LiteGraph.WIDGET_BGCOLOR = n.widgetBackground;
|
||
LiteGraph.WIDGET_OUTLINE_COLOR = n.widgetBackground;
|
||
// Nodes 2.0 renders DOM nodes and reads CSS tokens instead of canvas statics.
|
||
const root = document.documentElement;
|
||
root.style.setProperty("--component-node-background", n.bodyColor);
|
||
root.style.setProperty("--node-component-header-surface", n.titleColor);
|
||
root.style.setProperty("--component-node-widget-background", n.widgetBackground);
|
||
root.style.setProperty("--linear-node-widget-bg", n.widgetBackground);
|
||
root.style.setProperty("--linear-node-title-text", n.titleTextColor);
|
||
root.style.setProperty("--linear-node-shadow", this.config.canvas.fpsBoost || !n.shadowOpacity
|
||
? "none" : `drop-shadow(0 2px 4px rgba(0,0,0,${n.shadowOpacity}))`);
|
||
|
||
|
||
}
|
||
|
||
applySlots() {
|
||
const canvas = comfyApp.canvas;
|
||
if (!canvas) return;
|
||
if (!canvas.default_connection_color_byType) canvas.default_connection_color_byType = {};
|
||
for (const [type, color] of Object.entries({ ...THEME.SLOT_COLORS, ...this.config.slots })) {
|
||
canvas.default_connection_color_byType[type] = color;
|
||
document.documentElement.style.setProperty(`--color-datatype-${type}`, color);
|
||
}
|
||
}
|
||
|
||
applyUI() {
|
||
const u = this.config.ui;
|
||
const root = document.documentElement;
|
||
for (const [key, vars] of Object.entries(UI_VAR_MAP)) {
|
||
const val = u[key];
|
||
if (!val) continue;
|
||
for (const v of vars) {
|
||
if (v === "--p-focus-ring-color") {
|
||
root.style.setProperty(v, val + "66");
|
||
} else {
|
||
root.style.setProperty(v, val);
|
||
}
|
||
}
|
||
}
|
||
// Bars — solid background (no glassmorphism)
|
||
root.style.setProperty("--linear-bars-bg", u.barsBackground || "#1a1a1a");
|
||
}
|
||
|
||
_buildDotGrid(opacity) {
|
||
const canvas = comfyApp.canvas;
|
||
if (!canvas) return;
|
||
if (opacity <= 0) {
|
||
// Solid 1px tile instead of empty — fixes LiteGraph bg color bug at certain zoom levels
|
||
const solid = document.createElement("canvas");
|
||
solid.width = 1; solid.height = 1;
|
||
const sctx = solid.getContext("2d");
|
||
sctx.fillStyle = this.config.canvas.backgroundColor || "#0d0d0d";
|
||
sctx.fillRect(0, 0, 1, 1);
|
||
currentDotDataUrl = solid.toDataURL();
|
||
} else {
|
||
const tileSize = 100, dotSpacing = 10;
|
||
const c = document.createElement("canvas");
|
||
c.width = tileSize; c.height = tileSize;
|
||
const ctx = c.getContext("2d");
|
||
ctx.fillStyle = `rgba(255,255,255,${opacity})`;
|
||
for (let gy = 0; gy < tileSize; gy += dotSpacing) {
|
||
for (let gx = 0; gx < tileSize; gx += dotSpacing) {
|
||
ctx.fillRect(gx, gy, 1, 1);
|
||
}
|
||
}
|
||
currentDotDataUrl = c.toDataURL();
|
||
}
|
||
canvas.background_image = currentDotDataUrl;
|
||
canvas._pattern = null;
|
||
canvas._pattern_img = null;
|
||
|
||
if (window.LiteGraph) {
|
||
Object.defineProperty(LiteGraph, "BACKGROUND_IMAGE", {
|
||
get() { return currentDotDataUrl; },
|
||
set() {},
|
||
configurable: true
|
||
});
|
||
}
|
||
}
|
||
|
||
// ── Persistence ──
|
||
|
||
async _loadConfig() {
|
||
try {
|
||
const resp = await fetch("/linear-theme/config");
|
||
if (resp.ok) {
|
||
const saved = await resp.json();
|
||
if (saved && Object.keys(saved).length > 0) {
|
||
this.config = normalizeThemeConfig(saved);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn("[LinearTheme] Failed to load config:", e);
|
||
}
|
||
}
|
||
|
||
saveConfig() {
|
||
this._pendingSave = JSON.stringify(this.config);
|
||
if (this._savePromise) return this._savePromise;
|
||
this._savePromise = this._flushConfigSaves();
|
||
return this._savePromise;
|
||
}
|
||
|
||
async _flushConfigSaves() {
|
||
let saved = true;
|
||
try {
|
||
while (this._pendingSave !== null) {
|
||
const body = this._pendingSave;
|
||
this._pendingSave = null;
|
||
this._setSaveStatus("Saving…");
|
||
try {
|
||
const response = await fetch("/linear-theme/config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body,
|
||
});
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
saved = true;
|
||
this._setSaveStatus("");
|
||
} catch (error) {
|
||
saved = false;
|
||
this._setSaveStatus("Not saved", true);
|
||
console.warn("[LinearTheme] Failed to save config:", error);
|
||
}
|
||
}
|
||
return saved;
|
||
} finally {
|
||
this._savePromise = null;
|
||
}
|
||
}
|
||
|
||
_setSaveStatus(text, error = false) {
|
||
const status = this.panel?.querySelector(".te-save-status");
|
||
if (!status) return;
|
||
status.textContent = text;
|
||
status.classList.toggle("error", error);
|
||
const retry = this.panel.querySelector(".te-save-retry");
|
||
if (retry) retry.hidden = !error;
|
||
}
|
||
|
||
resetConfig() {
|
||
this.config = JSON.parse(JSON.stringify(THEME_DEFAULTS));
|
||
this.applyConfig();
|
||
this._syncInputsToConfig();
|
||
}
|
||
|
||
_loadPosition() {
|
||
try {
|
||
const raw = localStorage.getItem("linearTheme.panelPos");
|
||
const pos = raw ? JSON.parse(raw) : null;
|
||
return pos && Number.isFinite(pos.x) && Number.isFinite(pos.y) ? pos : null;
|
||
} catch { return null; }
|
||
}
|
||
|
||
_savePosition(x, y) {
|
||
try {
|
||
localStorage.setItem("linearTheme.panelPos", JSON.stringify({ x, y }));
|
||
} catch {}
|
||
}
|
||
|
||
_loadSections() {
|
||
try {
|
||
const raw = localStorage.getItem("linearTheme.sections");
|
||
const sections = raw ? JSON.parse(raw) : null;
|
||
return sections && typeof sections === "object" && !Array.isArray(sections) ? sections : { canvas: true };
|
||
} catch { return { canvas: true }; }
|
||
}
|
||
|
||
_saveSections() {
|
||
if (!this.panel) return;
|
||
const state = {};
|
||
this.panel.querySelectorAll(".te-section").forEach(s => {
|
||
state[s.dataset.section] = s.classList.contains("open");
|
||
});
|
||
try {
|
||
localStorage.setItem("linearTheme.sections", JSON.stringify(state));
|
||
} catch {}
|
||
}
|
||
|
||
// ── Events ──
|
||
|
||
_onInputChange(e) {
|
||
const el = e.target;
|
||
const { section, key } = el.dataset;
|
||
if (!section || !key) return;
|
||
if (el.type === "checkbox") this.config[section][key] = el.checked;
|
||
else if (el.type === "range") {
|
||
this.config[section][key] = Number(el.value);
|
||
const label = el.parentElement.querySelector(".te-range-val");
|
||
if (label) label.textContent = el.value;
|
||
} else if (el.type === "color") this.config[section][key] = el.value;
|
||
else return;
|
||
this._updateModifiedState();
|
||
this._pendingSections.add(section);
|
||
if (this._applyFrame === null) {
|
||
this._applyFrame = requestAnimationFrame(() => this._flushSections());
|
||
}
|
||
}
|
||
|
||
_flushSections() {
|
||
if (this._applyFrame !== null) cancelAnimationFrame(this._applyFrame);
|
||
this._applyFrame = null;
|
||
if (!this._pendingSections.size) return;
|
||
const sections = [...this._pendingSections];
|
||
this._pendingSections.clear();
|
||
for (const section of sections) this._applySection(section, false);
|
||
this.saveConfig();
|
||
comfyApp.canvas?.setDirty(true, true);
|
||
}
|
||
|
||
_applySection(section, persist = true) {
|
||
switch (section) {
|
||
case "canvas": this.applyCanvas(); break;
|
||
case "nodes": this.applyNodes(); break;
|
||
case "slots": this.applySlots(); break;
|
||
case "ui": this.applyUI(); break;
|
||
}
|
||
if (persist) {
|
||
this.saveConfig();
|
||
comfyApp.canvas?.setDirty(true, true);
|
||
}
|
||
}
|
||
|
||
_syncInputsToConfig() {
|
||
if (!this.panel) return;
|
||
this.panel.querySelectorAll("[data-section][data-key]").forEach(el => {
|
||
const val = this.config[el.dataset.section]?.[el.dataset.key];
|
||
if (val === undefined) return;
|
||
if (el.type === "color") {
|
||
el.value = val;
|
||
} else if (el.type === "range") {
|
||
el.value = val;
|
||
const valSpan = el.parentElement.querySelector(".te-range-val");
|
||
if (valSpan) valSpan.textContent = val;
|
||
} else if (el.type === "checkbox") {
|
||
el.checked = val;
|
||
}
|
||
});
|
||
this._updateModifiedState();
|
||
}
|
||
|
||
_updateModifiedState() {
|
||
if (!this.panel) return;
|
||
this.panel.querySelectorAll(".te-row").forEach(row => {
|
||
const input = row.querySelector("[data-section][data-key]");
|
||
if (!input) return;
|
||
const section = input.dataset.section;
|
||
const key = input.dataset.key;
|
||
const current = this.config[section]?.[key];
|
||
const def = THEME_DEFAULTS[section]?.[key];
|
||
const isModified = current !== def;
|
||
row.classList.toggle("modified", isModified);
|
||
const reset = row.querySelector(".te-field-reset");
|
||
if (reset) reset.disabled = !isModified;
|
||
});
|
||
}
|
||
|
||
// ── Presets ──
|
||
|
||
_getPresets() {
|
||
try {
|
||
const raw = localStorage.getItem("linearTheme.presets");
|
||
const presets = raw ? JSON.parse(raw) : [];
|
||
return Array.isArray(presets)
|
||
? presets.filter(p => p && typeof p.name === "string" && p.config && typeof p.config === "object" && !Array.isArray(p.config))
|
||
: [];
|
||
} catch { return []; }
|
||
}
|
||
|
||
_setPresets(presets) {
|
||
try {
|
||
localStorage.setItem("linearTheme.presets", JSON.stringify(presets));
|
||
} catch {}
|
||
}
|
||
|
||
_savePreset(name) {
|
||
const presets = this._getPresets();
|
||
const existing = presets.findIndex(p => p.name === name);
|
||
const entry = { name, config: JSON.parse(JSON.stringify(this.config)) };
|
||
if (existing >= 0) {
|
||
presets[existing] = entry;
|
||
} else {
|
||
presets.push(entry);
|
||
}
|
||
this._setPresets(presets);
|
||
this._renderPresets();
|
||
}
|
||
|
||
_loadPreset(name) {
|
||
const presets = this._getPresets();
|
||
const preset = presets.find(p => p.name === name);
|
||
if (!preset) return;
|
||
this.config = normalizeThemeConfig(preset.config);
|
||
this.applyConfig();
|
||
this._syncInputsToConfig();
|
||
}
|
||
|
||
_deletePreset(name) {
|
||
const presets = this._getPresets().filter(p => p.name !== name);
|
||
this._setPresets(presets);
|
||
this._renderPresets();
|
||
}
|
||
|
||
_renderPresets() {
|
||
if (!this._presetListEl) return;
|
||
const presets = this._getPresets();
|
||
if (presets.length === 0) {
|
||
this._presetListEl.innerHTML = `<div class="te-preset-empty">No saved presets</div>`;
|
||
return;
|
||
}
|
||
this._presetListEl.replaceChildren();
|
||
for (const preset of presets) {
|
||
const item = document.createElement("div");
|
||
item.className = "te-preset-item";
|
||
item.dataset.preset = preset.name;
|
||
const load = document.createElement("button");
|
||
load.className = "te-preset-item-name";
|
||
load.textContent = preset.name;
|
||
load.title = `Load ${preset.name}`;
|
||
load.addEventListener("click", () => this._loadPreset(preset.name));
|
||
const remove = document.createElement("button");
|
||
remove.className = "te-btn te-preset-del";
|
||
remove.textContent = "✕";
|
||
remove.title = `Delete ${preset.name}`;
|
||
remove.setAttribute("aria-label", remove.title);
|
||
remove.addEventListener("click", () => this._deletePreset(preset.name));
|
||
item.append(load, remove);
|
||
this._presetListEl.appendChild(item);
|
||
}
|
||
}
|
||
|
||
// ── Drag ──
|
||
|
||
_setupDrag(panel) {
|
||
const header = panel.querySelector(".te-header");
|
||
let dragging = false, startX, startY, origX, origY;
|
||
|
||
header.addEventListener("pointerdown", (e) => {
|
||
if (e.target.closest("button, input")) return;
|
||
dragging = true;
|
||
startX = e.clientX; startY = e.clientY;
|
||
const rect = panel.getBoundingClientRect();
|
||
origX = rect.left; origY = rect.top;
|
||
header.setPointerCapture(e.pointerId);
|
||
});
|
||
|
||
header.addEventListener("pointermove", (e) => {
|
||
if (!dragging) return;
|
||
const dx = e.clientX - startX;
|
||
const dy = e.clientY - startY;
|
||
panel.style.left = (origX + dx) + "px";
|
||
panel.style.top = (origY + dy) + "px";
|
||
panel.style.right = "auto";
|
||
});
|
||
|
||
header.addEventListener("pointerup", (e) => {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
header.releasePointerCapture(e.pointerId);
|
||
this._clampPosition();
|
||
const rect = panel.getBoundingClientRect();
|
||
this._savePosition(rect.left, rect.top);
|
||
});
|
||
}
|
||
}
|
||
|
||
const themeEditor = new ThemeEditor();
|
||
|
||
// Offscreen canvas for measuring text width
|
||
const _measureCtx = document.createElement("canvas").getContext("2d");
|
||
const NODE_LAYOUT_GRID = 10;
|
||
const NODE_FONT_FAMILY = "Inter, Arial, sans-serif";
|
||
let nodeFontRevision = 0;
|
||
document.fonts?.addEventListener("loadingdone", () => {
|
||
nodeFontRevision++;
|
||
comfyApp.canvas?.setDirty(true, true);
|
||
});
|
||
|
||
function getCollapsedTitleWidth(node) {
|
||
const title = (node.getTitle ? node.getTitle() : node.title) || "";
|
||
const fontSize = LiteGraph.NODE_TEXT_SIZE || 14;
|
||
_measureCtx.font = `${fontSize}px Inter, Arial, sans-serif`;
|
||
const textW = _measureCtx.measureText(title).width;
|
||
const textX = 20; // after collapse arrow
|
||
const slotR = 20; // space for output dot
|
||
return textX + textW + slotR + 10;
|
||
}
|
||
|
||
function enforceTitleMinWidth(node) {
|
||
let measureKey = null;
|
||
let minWidth = 0;
|
||
function getMinW() {
|
||
const title = (node.getTitle ? node.getTitle() : node.title) || "";
|
||
const fontSize = LiteGraph.NODE_TEXT_SIZE || 14;
|
||
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
||
const key = JSON.stringify([title, fontSize, titleH, nodeFontRevision, document.fonts?.status]);
|
||
if (key !== measureKey) {
|
||
measureKey = key;
|
||
_measureCtx.font = `${fontSize}px ${NODE_FONT_FAMILY}`;
|
||
minWidth = _measureCtx.measureText(title).width + titleH * 2 + 4;
|
||
}
|
||
return minWidth;
|
||
}
|
||
|
||
// Intercept _size (used by renderingSize getter) to clamp width & snap to grid
|
||
const GRID = NODE_LAYOUT_GRID;
|
||
function snap(w) { return Math.ceil(w / GRID) * GRID; }
|
||
|
||
let _sizeVal = node._size;
|
||
if (!node.flags?.collapsed) _sizeVal[0] = snap(Math.max(_sizeVal[0], getMinW()));
|
||
Object.defineProperty(node, '_size', {
|
||
get() {
|
||
if (!node.flags?.collapsed) {
|
||
const minW = getMinW();
|
||
if (_sizeVal[0] < minW) _sizeVal[0] = snap(minW);
|
||
else _sizeVal[0] = snap(_sizeVal[0]);
|
||
_sizeVal[1] = snap(_sizeVal[1]);
|
||
}
|
||
return _sizeVal;
|
||
},
|
||
set(v) {
|
||
_sizeVal = v;
|
||
if (!node.flags?.collapsed) {
|
||
const minW = getMinW();
|
||
if (_sizeVal[0] < minW) _sizeVal[0] = snap(minW);
|
||
else _sizeVal[0] = snap(_sizeVal[0]);
|
||
_sizeVal[1] = snap(_sizeVal[1]);
|
||
}
|
||
},
|
||
configurable: true
|
||
});
|
||
}
|
||
|
||
function lockCollapsedWidth(node) {
|
||
const GRID = NODE_LAYOUT_GRID;
|
||
function snap(w) { return Math.ceil(w / GRID) * GRID; }
|
||
let stored = node._collapsed_width || 0;
|
||
let measuredTitle = null;
|
||
let measuredFontSize = null;
|
||
let measuredFontRevision = null;
|
||
let measuredFontStatus = null;
|
||
let titleW = 0;
|
||
Object.defineProperty(node, '_collapsed_width', {
|
||
get() {
|
||
const title = (node.getTitle ? node.getTitle() : node.title) || "";
|
||
const fontSize = LiteGraph.NODE_TEXT_SIZE || 14;
|
||
if (title !== measuredTitle || fontSize !== measuredFontSize
|
||
|| nodeFontRevision !== measuredFontRevision || document.fonts?.status !== measuredFontStatus) {
|
||
measuredTitle = title;
|
||
measuredFontSize = fontSize;
|
||
measuredFontRevision = nodeFontRevision;
|
||
measuredFontStatus = document.fonts?.status;
|
||
titleW = getCollapsedTitleWidth(node);
|
||
}
|
||
return snap(Math.max(stored, titleW));
|
||
},
|
||
set(v) { stored = v; },
|
||
configurable: true
|
||
});
|
||
}
|
||
|
||
// ── Group color palette (muted zinc tones) ──
|
||
const GROUP_FILL_ALPHA = 0.07; // subtle tint that keeps the chosen color visible
|
||
const GROUP_BORDER_ALPHA = 0.35;
|
||
|
||
const LINEAR_GROUP_COLORS = {
|
||
'#335': { bg: '#2a2a2e', accent: '#a1a1aa' }, // default (no color) → zinc
|
||
'#8AA': { bg: '#2d4a4a', accent: '#2dd4bf' }, // teal/cyan
|
||
'#A88': { bg: '#4a2d2d', accent: '#f87171' }, // red/rose
|
||
'#b06634': { bg: '#4a3320', accent: '#fb923c' }, // brown/amber
|
||
'#8A8': { bg: '#2d4a2d', accent: '#4ade80' }, // green/emerald
|
||
'#88A': { bg: '#2d2d4a', accent: '#60a5fa' }, // blue
|
||
'#3f789e': { bg: '#1e3a4d', accent: '#38bdf8' }, // pale blue/sky
|
||
'#a1309b': { bg: '#3d1e3d', accent: '#c084fc' }, // purple
|
||
'#b58b2a': { bg: '#3d3520', accent: '#fbbf24' }, // yellow/gold
|
||
};
|
||
|
||
function _hexToRgb(hex) {
|
||
hex = hex.replace('#', '');
|
||
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||
return {
|
||
r: parseInt(hex.substring(0, 2), 16),
|
||
g: parseInt(hex.substring(2, 4), 16),
|
||
b: parseInt(hex.substring(4, 6), 16)
|
||
};
|
||
}
|
||
|
||
function installGroupOverride() {
|
||
if (!LGraphCanvas.prototype._origDrawGroups) {
|
||
LGraphCanvas.prototype._origDrawGroups = LGraphCanvas.prototype.drawGroups;
|
||
}
|
||
|
||
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
|
||
if (!this.graph) return;
|
||
const groups = this.graph._groups;
|
||
if (!groups || groups.length === 0) return;
|
||
|
||
ctx.save();
|
||
const scale = this.ds?.scale || 1;
|
||
|
||
// Skip groups entirely when zoomed out very far
|
||
if (scale < 0.2) { ctx.restore(); return; }
|
||
|
||
for (let i = 0; i < groups.length; i++) {
|
||
const group = groups[i];
|
||
if (!group) continue;
|
||
|
||
const pos = group._pos || group.pos;
|
||
const size = group._size || group.size;
|
||
if (!pos || !size) continue;
|
||
|
||
const rawColor = group.color || '#335';
|
||
const palette = LINEAR_GROUP_COLORS[rawColor] || { bg: rawColor, accent: rawColor };
|
||
const accentRgb = _hexToRgb(palette.accent);
|
||
// Explicit group colors win. The editor controls only the default tint.
|
||
const fillRgb = rawColor === '#335'
|
||
? _hexToRgb(themeEditor.config.canvas.groupColor)
|
||
: accentRgb;
|
||
|
||
const x = pos[0], y = pos[1], w = size[0], h = size[1];
|
||
const r = 8;
|
||
|
||
// ── Subtle color fill + 1px border ──
|
||
// The fill keeps the user's chosen color visible (GitHub issue #1
|
||
// "group color not working") without leaving the minimal look.
|
||
ctx.beginPath();
|
||
ctx.roundRect(x, y, w, h, r);
|
||
ctx.fillStyle = `rgba(${fillRgb.r}, ${fillRgb.g}, ${fillRgb.b}, ${GROUP_FILL_ALPHA})`;
|
||
ctx.fill();
|
||
ctx.strokeStyle = `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, ${GROUP_BORDER_ALPHA})`;
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
|
||
// ── Small color dot ──
|
||
const dotR = 3;
|
||
const dotX = x + 12, dotY = y + 14;
|
||
ctx.beginPath();
|
||
ctx.arc(dotX, dotY, dotR, 0, Math.PI * 2);
|
||
ctx.fillStyle = `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.8)`;
|
||
ctx.fill();
|
||
|
||
// ── Title text — simple, no shadow ──
|
||
if (scale > 0.3) {
|
||
const fontSize = 14;
|
||
ctx.font = `500 ${fontSize}px Inter, Arial, sans-serif`;
|
||
ctx.fillStyle = `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.6)`;
|
||
ctx.textAlign = 'left';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(group.title || 'Group', x + 22, y + 14);
|
||
}
|
||
}
|
||
|
||
ctx.restore();
|
||
};
|
||
}
|
||
|
||
const LINEAR_THEME_EXTENSION = {
|
||
name: "Comfy.LinearTheme",
|
||
nodeCreated(node) {
|
||
enforceTitleMinWidth(node);
|
||
lockCollapsedWidth(node);
|
||
},
|
||
loadedGraphNode(node) {
|
||
enforceTitleMinWidth(node);
|
||
lockCollapsedWidth(node);
|
||
},
|
||
getCanvasMenuItems(canvas) {
|
||
const selectedNodes = canvas.selectedItems
|
||
? [...canvas.selectedItems].filter(item => typeof item.collapse === "function")
|
||
: Object.values(canvas.selected_nodes || {});
|
||
const hasSelection = selectedNodes.length > 0;
|
||
|
||
return [null, {
|
||
content: "Linear Theme",
|
||
has_submenu: true,
|
||
submenu: {
|
||
options: [
|
||
{
|
||
content: "Compact Selected Nodes",
|
||
disabled: !hasSelection,
|
||
callback: () => {
|
||
for (const node of selectedNodes) {
|
||
if (!node.flags.collapsed) {
|
||
const minSize = node.computeSize();
|
||
node.setSize(minSize);
|
||
}
|
||
}
|
||
comfyApp.canvas.setDirty(true, true);
|
||
}
|
||
},
|
||
{
|
||
content: "Collapse Selected Nodes",
|
||
disabled: !hasSelection,
|
||
callback: () => {
|
||
for (const node of selectedNodes) {
|
||
if (!node.flags.collapsed) {
|
||
node.collapse();
|
||
}
|
||
}
|
||
comfyApp.canvas.setDirty(true, true);
|
||
}
|
||
},
|
||
{
|
||
content: "Arrange Selected Nodes",
|
||
disabled: !hasSelection,
|
||
callback: () => {
|
||
arrangeSelectedNodes(selectedNodes);
|
||
}
|
||
},
|
||
null, // separator
|
||
{
|
||
content: "Expand Selected Nodes",
|
||
disabled: !hasSelection,
|
||
callback: () => {
|
||
for (const node of selectedNodes) {
|
||
if (node.flags.collapsed) {
|
||
node.collapse(); // toggles back
|
||
}
|
||
}
|
||
comfyApp.canvas.setDirty(true, true);
|
||
}
|
||
},
|
||
null, // separator
|
||
{
|
||
content: "Theme Editor",
|
||
callback: () => {
|
||
themeEditor.toggle();
|
||
}
|
||
},
|
||
],
|
||
},
|
||
}];
|
||
},
|
||
async setup() {
|
||
// Re-inject CSS so our <style> is last in <head> (wins specificity ties
|
||
// against custom node styles loaded after module import)
|
||
injectLinearThemeCSS();
|
||
|
||
// Apply LiteGraph canvas theme
|
||
const canvas = comfyApp.canvas;
|
||
if (canvas) {
|
||
// Performance optimizations
|
||
canvas.render_connections_border = false;
|
||
canvas.render_connection_arrows = false;
|
||
canvas.highquality_render = false;
|
||
canvas.render_shadows = false;
|
||
canvas.show_info = false;
|
||
canvas.render_execution_order = false;
|
||
canvas.render_collapsed_slots = false;
|
||
canvas.use_gradients = false;
|
||
canvas.always_render_background = false;
|
||
// links_render_mode is NOT forced here — the user's own
|
||
// Comfy.LinkRenderMode setting stays in control (GitHub issue #1).
|
||
|
||
/* ═══════════════════════════════════════════════════════
|
||
FPS Boost — Runtime rendering optimizations
|
||
═══════════════════════════════════════════════════════ */
|
||
|
||
// 1. Use the current LiteGraph frame limiter, respecting FPS Boost.
|
||
{
|
||
let idleTimer = null;
|
||
let isIdle = false;
|
||
const normalGap = canvas._maximumFrameGap || 0;
|
||
const normalMaxFps = normalGap > 0 ? 1000 / normalGap : 0;
|
||
const supportsLimiter = "maximumFps" in canvas;
|
||
const updateLimit = () => {
|
||
if (!supportsLimiter) return;
|
||
const idle = isIdle && themeEditor.config.canvas.fpsBoost
|
||
&& executionState.runningNodeId == null;
|
||
canvas.maximumFps = idle ? (normalMaxFps ? Math.min(15, normalMaxFps) : 15) : normalMaxFps;
|
||
};
|
||
const wakeUp = () => {
|
||
isIdle = false;
|
||
updateLimit();
|
||
clearTimeout(idleTimer);
|
||
idleTimer = setTimeout(() => {
|
||
isIdle = true;
|
||
updateLimit();
|
||
}, 800);
|
||
};
|
||
themeEditor._updateIdleRendering = updateLimit;
|
||
const canvasEl = canvas.canvas;
|
||
if (canvasEl) {
|
||
for (const event of ["pointermove", "pointerdown", "pointerup", "wheel"]) {
|
||
canvasEl.addEventListener(event, wakeUp, { passive: true });
|
||
}
|
||
document.addEventListener("keydown", wakeUp, { passive: true });
|
||
}
|
||
for (const event of ["execution_start", "executing", "execution_error", "execution_success"]) {
|
||
api?.addEventListener(event, wakeUp);
|
||
}
|
||
wakeUp();
|
||
}
|
||
|
||
// 2. Skip widget text rendering when zoomed out far
|
||
// Below 0.5 zoom, widgets are too small to read anyway.
|
||
{
|
||
const origDrawNodeWidgets = LGraphCanvas.prototype.drawNodeWidgets;
|
||
if (origDrawNodeWidgets) {
|
||
LGraphCanvas.prototype.drawNodeWidgets = function(node, posY, ctx, active_widget) {
|
||
if (themeEditor.config.canvas.fpsBoost && this.ds && this.ds.scale < 0.4) return;
|
||
return origDrawNodeWidgets.apply(this, arguments);
|
||
};
|
||
}
|
||
}
|
||
|
||
// 3. Simplify link rendering during pan/drag + draw chevron arrows at inputs
|
||
{
|
||
const origDrawConnections = LGraphCanvas.prototype.drawConnections;
|
||
if (origDrawConnections) {
|
||
LGraphCanvas.prototype.drawConnections = function(ctx) {
|
||
// Simplify during drag
|
||
if (this.dragging_canvas) {
|
||
const prevBorder = this.render_connections_border;
|
||
this.render_connections_border = false;
|
||
try {
|
||
return origDrawConnections.call(this, ctx);
|
||
} finally {
|
||
this.render_connections_border = prevBorder;
|
||
}
|
||
}
|
||
const result = origDrawConnections.call(this, ctx);
|
||
|
||
// Draw chevron arrows at input endpoints
|
||
const graph = this.graph;
|
||
if (!graph || !graph._links) return result;
|
||
|
||
const scale = this.ds?.scale || 1;
|
||
if (scale < 0.25) return result; // too zoomed out
|
||
|
||
// Viewport bounds for culling
|
||
const vx = this.visible_area?.[0] ?? 0;
|
||
const vy = this.visible_area?.[1] ?? 0;
|
||
const vw = this.visible_area?.[2] ?? 1e6;
|
||
const vh = this.visible_area?.[3] ?? 1e6;
|
||
const vx2 = vx + vw;
|
||
const vy2 = vy + vh;
|
||
|
||
const chevSize = Math.max(4, Math.min(8, 6 * scale));
|
||
const defaultColor = LiteGraph.LINK_COLOR || "#52525b";
|
||
const byType = this.default_connection_color_byType || {};
|
||
|
||
ctx.save();
|
||
ctx.lineWidth = 1.5 * scale;
|
||
ctx.lineCap = "round";
|
||
ctx.lineJoin = "round";
|
||
|
||
for (const id in graph._links) {
|
||
const link = graph._links[id];
|
||
if (!link) continue;
|
||
|
||
const srcNode = graph.getNodeById(link.origin_id);
|
||
const dstNode = graph.getNodeById(link.target_id);
|
||
if (!srcNode || !dstNode) continue;
|
||
|
||
const endPos = dstNode.getConnectionPos?.(true, link.target_slot);
|
||
if (!endPos) continue;
|
||
|
||
// Viewport cull on input endpoint
|
||
if (endPos[0] < vx || endPos[0] > vx2 || endPos[1] < vy || endPos[1] > vy2) continue;
|
||
|
||
const startPos = srcNode.getConnectionPos?.(false, link.origin_slot);
|
||
if (!startPos) continue;
|
||
|
||
// Direction vector
|
||
const dx = endPos[0] - startPos[0];
|
||
const dy = endPos[1] - startPos[1];
|
||
const len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len < 30) continue; // too short for a chevron
|
||
|
||
const nx = dx / len;
|
||
const ny = dy / len;
|
||
|
||
// Chevron tip position (slightly before input slot)
|
||
const tipX = endPos[0] - nx * 4;
|
||
const tipY = endPos[1] - ny * 4;
|
||
|
||
// Chevron arms (open V pointing toward input)
|
||
const armX = nx * chevSize;
|
||
const armY = ny * chevSize;
|
||
const perpX = ny * chevSize * 0.5;
|
||
const perpY = -nx * chevSize * 0.5;
|
||
|
||
ctx.strokeStyle = byType[link.type] || defaultColor;
|
||
ctx.beginPath();
|
||
ctx.moveTo(tipX - armX + perpX, tipY - armY + perpY);
|
||
ctx.lineTo(tipX, tipY);
|
||
ctx.lineTo(tipX - armX - perpX, tipY - armY - perpY);
|
||
ctx.stroke();
|
||
}
|
||
|
||
ctx.restore();
|
||
return result;
|
||
};
|
||
}
|
||
}
|
||
|
||
// 4. Skip title rendering when zoomed out very far
|
||
{
|
||
const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
|
||
if (origDrawNodeShape) {
|
||
LGraphCanvas.prototype.drawNodeShape = function(node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
|
||
// At very low zoom, skip text-heavy rendering
|
||
if (themeEditor.config.canvas.fpsBoost && this.ds && this.ds.scale < 0.3 && !selected) {
|
||
// Just draw a simple rectangle
|
||
ctx.fillStyle = bgcolor || "#1a1a1a";
|
||
const r = 8 * this.ds.scale;
|
||
ctx.beginPath();
|
||
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT, size[0], size[1] + LiteGraph.NODE_TITLE_HEIGHT, r);
|
||
ctx.fill();
|
||
if (selected) {
|
||
ctx.strokeStyle = "#ffffff";
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
}
|
||
return;
|
||
}
|
||
return origDrawNodeShape.call(this, node, ctx, size, fgcolor, bgcolor, selected, mouse_over);
|
||
};
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════
|
||
End FPS Boost
|
||
═══════════════════════════════════════════════════════ */
|
||
|
||
for (const [key, value] of Object.entries(THEME)) {
|
||
if (key === "SLOT_COLORS") continue;
|
||
if (canvas[key] !== undefined) {
|
||
canvas[key] = value;
|
||
}
|
||
}
|
||
|
||
// Build dot grid only if opacity > 0 (skip for performance when dots disabled)
|
||
if (THEME_DEFAULTS.canvas.dotGridOpacity > 0) {
|
||
const tileSize = 100, dotSpacing = 10;
|
||
const dotCanvas = document.createElement("canvas");
|
||
dotCanvas.width = tileSize; dotCanvas.height = tileSize;
|
||
const dotCtx = dotCanvas.getContext("2d");
|
||
dotCtx.fillStyle = "rgba(255,255,255,0.5)";
|
||
for (let gy = 0; gy < tileSize; gy += dotSpacing) {
|
||
for (let gx = 0; gx < tileSize; gx += dotSpacing) {
|
||
dotCtx.fillRect(gx, gy, 1, 1);
|
||
}
|
||
}
|
||
currentDotDataUrl = dotCanvas.toDataURL();
|
||
} else {
|
||
// Create a solid 1px tile instead of empty string
|
||
// Empty BACKGROUND_IMAGE causes LiteGraph to render wrong bg color at certain zoom levels
|
||
// (see litegraph.js issues #276, #468)
|
||
const solidCanvas = document.createElement("canvas");
|
||
solidCanvas.width = 1; solidCanvas.height = 1;
|
||
const solidCtx = solidCanvas.getContext("2d");
|
||
solidCtx.fillStyle = THEME_DEFAULTS.canvas.backgroundColor || "#0d0d0d";
|
||
solidCtx.fillRect(0, 0, 1, 1);
|
||
currentDotDataUrl = solidCanvas.toDataURL();
|
||
}
|
||
|
||
canvas.background_image = currentDotDataUrl;
|
||
canvas._pattern = null;
|
||
canvas._pattern_img = null;
|
||
|
||
// Lock BACKGROUND_IMAGE so palette loading can't overwrite it
|
||
if (window.LiteGraph) {
|
||
Object.defineProperty(LiteGraph, "BACKGROUND_IMAGE", {
|
||
get() { return currentDotDataUrl; },
|
||
set() {},
|
||
configurable: true
|
||
});
|
||
}
|
||
}
|
||
|
||
// Apply theme properties to LiteGraph statics (CONNECTING_LINK_COLOR, LINK_COLOR, etc.)
|
||
if (window.LiteGraph) {
|
||
for (const [key, value] of Object.entries(THEME)) {
|
||
if (key === "SLOT_COLORS") continue;
|
||
if (typeof value === "string" && LiteGraph[key] !== undefined) {
|
||
LiteGraph[key] = value;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Hide LiteGraph slot dots on collapsed nodes & store real slots for themed drawing
|
||
const origDrawNode = LGraphCanvas.prototype.drawNode;
|
||
LGraphCanvas.prototype.drawNode = function(node, ctx) {
|
||
if (node.flags?.collapsed) {
|
||
const savedInputs = node.inputs;
|
||
const savedOutputs = node.outputs;
|
||
node._realInputs = savedInputs;
|
||
node._realOutputs = savedOutputs;
|
||
// Slot setters mutate their backing arrays in recent LiteGraph versions.
|
||
// Hide only the renderer's slot dots; never alter node data.
|
||
const savedRenderCollapsedSlots = this.render_collapsed_slots;
|
||
this.render_collapsed_slots = false;
|
||
try {
|
||
return origDrawNode.call(this, node, ctx);
|
||
} finally {
|
||
this.render_collapsed_slots = savedRenderCollapsedSlots;
|
||
}
|
||
}
|
||
return origDrawNode.call(this, node, ctx);
|
||
};
|
||
|
||
// Override group rendering with Linear-style groups
|
||
installGroupOverride();
|
||
|
||
// Force --bg-color on body (ComfyUI palette sets its own value on body)
|
||
document.body.style.setProperty("--bg-color", THEME.CLEAR_BACKGROUND_COLOR);
|
||
|
||
// Apply slot colors
|
||
if (window.LiteGraph) {
|
||
for (const [type, color] of Object.entries(THEME.SLOT_COLORS)) {
|
||
LiteGraph.registerNodeSlotColor?.(type, color);
|
||
}
|
||
}
|
||
|
||
// Dynamic CONNECTING_LINK_COLOR — reads the type from the link being
|
||
// dragged (connecting_links[0].output/input.type) and returns its
|
||
// byType color so links don't change color mid-drag
|
||
if (canvas && window.LiteGraph) {
|
||
const fallback = canvas.default_connection_color?.output_on || "#7F7";
|
||
Object.defineProperty(LiteGraph, "CONNECTING_LINK_COLOR", {
|
||
get() {
|
||
const c = comfyApp.canvas;
|
||
const links = c?.connecting_links || c?._connecting_links;
|
||
if (links && links.length > 0) {
|
||
const type = links[0].output?.type || links[0].input?.type;
|
||
if (type) {
|
||
const tc = c.default_connection_color_byType?.[type];
|
||
if (tc) return tc;
|
||
}
|
||
}
|
||
return fallback;
|
||
},
|
||
set() {},
|
||
configurable: true
|
||
});
|
||
}
|
||
|
||
// Patch image dimension text font (drawn as "10px sans-serif" by default)
|
||
const graphCanvas = document.querySelector("canvas");
|
||
if (graphCanvas) {
|
||
const ctx = graphCanvas.getContext("2d");
|
||
const origFillText = ctx.__proto__.fillText;
|
||
ctx.__proto__.fillText = function (text, x, y, maxWidth) {
|
||
if (typeof text === "string" && /^\d{2,5}\s*[×x]\s*\d{2,5}$/.test(text)) {
|
||
// save/restore: without it the font/fillStyle mutation
|
||
// leaks into every later draw on the same context
|
||
this.save();
|
||
this.font = "11px Inter, Arial, sans-serif";
|
||
this.fillStyle = "#71717a";
|
||
const r = maxWidth !== undefined
|
||
? origFillText.call(this, text, x, y, maxWidth)
|
||
: origFillText.call(this, text, x, y);
|
||
this.restore();
|
||
return r;
|
||
}
|
||
return maxWidth !== undefined
|
||
? origFillText.call(this, text, x, y, maxWidth)
|
||
: origFillText.call(this, text, x, y);
|
||
};
|
||
}
|
||
|
||
// --- Execution glow: API listeners ---
|
||
if (api) {
|
||
api.addEventListener("execution_start", () => {
|
||
executionState.runningNodeId = null;
|
||
const now = performance.now();
|
||
for (const id in executionState.completedNodes) {
|
||
if (now - executionState.completedNodes[id] > 700) {
|
||
delete executionState.completedNodes[id];
|
||
}
|
||
}
|
||
executionState.errorNodes = {};
|
||
});
|
||
|
||
api.addEventListener("executing", (e) => {
|
||
const nodeId = e?.detail;
|
||
if (executionState.runningNodeId && executionState.runningNodeId !== nodeId) {
|
||
executionState.completedNodes[executionState.runningNodeId] = performance.now();
|
||
comfyApp.canvas?.setDirty(true, false);
|
||
}
|
||
executionState.runningNodeId = nodeId || null;
|
||
if (nodeId) {
|
||
comfyApp.canvas?.setDirty(true, false);
|
||
}
|
||
if (!nodeId) {
|
||
executionState.runningNodeId = null;
|
||
}
|
||
});
|
||
|
||
api.addEventListener("execution_error", (e) => {
|
||
const nodeId = e?.detail?.node_id;
|
||
if (nodeId) {
|
||
executionState.errorNodes[nodeId] = performance.now();
|
||
executionState.runningNodeId = null;
|
||
comfyApp.canvas?.setDirty(true, false);
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Theme Editor: initialize & apply saved config ---
|
||
await themeEditor.init();
|
||
|
||
// --- Floating actionbar: idempotent styles, one scheduled pass per frame ---
|
||
{
|
||
let pendingFrame = null;
|
||
const setStyle = (element, property, value) => {
|
||
if (element.style.getPropertyValue(property) !== value
|
||
|| element.style.getPropertyPriority(property) !== "important") {
|
||
element.style.setProperty(property, value, "important");
|
||
}
|
||
};
|
||
const fixActionbar = () => {
|
||
const ab = document.querySelector(".actionbar");
|
||
if (!ab) return;
|
||
for (const element of [ab, ab.querySelector(".p-panel-content"), document.querySelector(".actionbar-container")]) {
|
||
if (!element) continue;
|
||
setStyle(element, "background-color", "rgb(19, 19, 19)");
|
||
setStyle(element, "border", "none");
|
||
setStyle(element, "border-radius", "10px");
|
||
}
|
||
const position = getComputedStyle(ab).position;
|
||
if (position === "fixed" || position === "absolute") {
|
||
const rect = ab.getBoundingClientRect();
|
||
const vw = document.documentElement.clientWidth;
|
||
const vh = document.documentElement.clientHeight;
|
||
if (rect.right > vw || rect.left < 0 || rect.bottom > vh || rect.top < 0) {
|
||
const left = Math.max(8, Math.min(rect.left, vw - rect.width - 8));
|
||
const top = Math.max(8, Math.min(rect.top, vh - rect.height - 8));
|
||
const parentRect = position === "absolute" ? ab.offsetParent?.getBoundingClientRect() : null;
|
||
ab.style.left = `${left - (parentRect?.left || 0)}px`;
|
||
ab.style.top = `${top - (parentRect?.top || 0)}px`;
|
||
}
|
||
}
|
||
};
|
||
const schedule = () => {
|
||
if (pendingFrame !== null) return;
|
||
pendingFrame = requestAnimationFrame(() => {
|
||
pendingFrame = null;
|
||
fixActionbar();
|
||
});
|
||
};
|
||
const target = document.querySelector(".actionbar-container") || document.querySelector(".actionbar");
|
||
if (target) {
|
||
fixActionbar();
|
||
new MutationObserver(schedule).observe(target, {
|
||
childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"],
|
||
});
|
||
window.addEventListener("resize", schedule);
|
||
}
|
||
}
|
||
|
||
// --- Snap Guides: alignment lines when dragging nodes ---
|
||
{
|
||
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
||
const origDrawFront = LGraphCanvas.prototype.drawFrontCanvas;
|
||
LGraphCanvas.prototype.drawFrontCanvas = function(...args) {
|
||
origDrawFront.apply(this, args);
|
||
|
||
// Check config — default to true if not set
|
||
if (themeEditor?.config?.canvas?.snapGuides === false) return;
|
||
|
||
const ctx = this.ctx;
|
||
if (!ctx) return;
|
||
const graph = this.graph;
|
||
if (!graph?._nodes) return;
|
||
|
||
// Only when actively dragging nodes (mouse down + selected nodes + mouse moving)
|
||
const selected = this.selected_nodes;
|
||
if (!selected || Object.keys(selected).length === 0) return;
|
||
if (!this.last_mouse_dragging && !this.node_dragged) return;
|
||
const d = Object.values(selected)[0];
|
||
|
||
const THRESH = 8;
|
||
const guides = [];
|
||
const dx = d.pos[0], dy = d.pos[1] - titleH;
|
||
const dw = d.size[0], dh = d.size[1] + titleH;
|
||
const dr = dx + dw, db = dy + dh;
|
||
const dcx = dx + dw/2, dcy = dy + dh/2;
|
||
|
||
for (const n of graph._nodes) {
|
||
if (n.id === d.id || selected[n.id]) continue;
|
||
const nx = n.pos[0], ny = n.pos[1] - titleH;
|
||
const nw = n.size[0], nh = n.size[1] + titleH;
|
||
const nr = nx + nw, nb = ny + nh;
|
||
const ncx = nx + nw/2, ncy = ny + nh/2;
|
||
if (Math.abs(dx - nx) < THRESH) guides.push({t:'v', v:nx});
|
||
if (Math.abs(dr - nr) < THRESH) guides.push({t:'v', v:nr});
|
||
if (Math.abs(dcx - ncx) < THRESH) guides.push({t:'v', v:ncx});
|
||
if (Math.abs(dy - ny) < THRESH) guides.push({t:'h', v:ny});
|
||
if (Math.abs(db - nb) < THRESH) guides.push({t:'h', v:nb});
|
||
if (Math.abs(dcy - ncy) < THRESH) guides.push({t:'h', v:ncy});
|
||
}
|
||
if (graph._groups) {
|
||
for (const g of graph._groups) {
|
||
if (!g) continue;
|
||
const gx = g.pos[0], gy = g.pos[1], gw = g.size[0], gh = g.size[1];
|
||
if (Math.abs(dx - gx) < THRESH) guides.push({t:'v', v:gx});
|
||
if (Math.abs(dr - (gx + gw)) < THRESH) guides.push({t:'v', v:gx + gw});
|
||
if (Math.abs(dy - gy) < THRESH) guides.push({t:'h', v:gy});
|
||
if (Math.abs(db - (gy + gh)) < THRESH) guides.push({t:'h', v:gy + gh});
|
||
}
|
||
}
|
||
if (guides.length === 0) return;
|
||
ctx.save();
|
||
const scale = this.ds.scale, offset = this.ds.offset;
|
||
ctx.strokeStyle = 'rgba(244,244,245,0.2)';
|
||
ctx.lineWidth = 1;
|
||
ctx.setLineDash([4, 4]);
|
||
const seen = new Set();
|
||
for (const g of guides) {
|
||
const key = g.t + Math.round(g.v);
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
const sv = g.t === 'v' ? (g.v + offset[0]) * scale : (g.v + offset[1]) * scale;
|
||
ctx.beginPath();
|
||
if (g.t === 'v') { ctx.moveTo(sv, 0); ctx.lineTo(sv, ctx.canvas.height); }
|
||
else { ctx.moveTo(0, sv); ctx.lineTo(ctx.canvas.width, sv); }
|
||
ctx.stroke();
|
||
}
|
||
ctx.restore();
|
||
};
|
||
}
|
||
|
||
// --- Auto-register the Linear Dark color palette (first install only) ---
|
||
// Served by __init__.py from linear_dark.json (single source). We add it
|
||
// to Comfy.CustomColorPalettes and activate it ONCE; if the user later
|
||
// picks another palette we never override their choice again.
|
||
try {
|
||
const s = comfyApp.extensionManager?.setting;
|
||
if (s) {
|
||
const existing = s.get("Comfy.CustomColorPalettes") ?? {};
|
||
if (!existing.linear_dark) {
|
||
const resp = await fetch("/linear-theme/palette");
|
||
if (resp.ok) {
|
||
const palette = await resp.json();
|
||
if (palette?.id === "linear_dark") {
|
||
await s.set("Comfy.CustomColorPalettes", { ...existing, linear_dark: palette });
|
||
await s.set("Comfy.ColorPalette", "linear_dark");
|
||
console.log("[LinearTheme] Linear Dark palette registered and set as default");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn("[LinearTheme] Could not auto-register palette:", e);
|
||
}
|
||
|
||
console.log(`[LinearTheme] Theme applied — v${LINEAR_THEME_VERSION}`);
|
||
},
|
||
|
||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||
const origDrawFg = nodeType.prototype.onDrawForeground;
|
||
|
||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||
try {
|
||
if (origDrawFg) origDrawFg.apply(this, arguments);
|
||
} catch(e) {
|
||
// warn once per node type — silent swallowing hid real node errors
|
||
if (!nodeType.__linearThemeFgWarned) {
|
||
nodeType.__linearThemeFgWarned = true;
|
||
console.warn("[LinearTheme] onDrawForeground error in", nodeData?.name, e);
|
||
}
|
||
}
|
||
|
||
const w = this.size[0];
|
||
const h = this.size[1];
|
||
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
||
const radius = themeEditor.config.canvas.nodeBorderRadius;
|
||
|
||
// Skip detailed drawing when zoomed out
|
||
const scale = comfyApp.canvas?.ds?.scale || 1;
|
||
if (scale < 0.4) return;
|
||
|
||
// Cover the anti-aliasing seam between title and body
|
||
if (!this.flags?.collapsed) {
|
||
ctx.fillStyle = this.bgcolor || themeEditor?.config?.nodes?.bodyColor || "#18181b";
|
||
ctx.fillRect(0, -1, w, 2);
|
||
}
|
||
|
||
// ── Collapsed: redraw title area (LiteGraph clips text to old width) ──
|
||
if (this.flags?.collapsed) {
|
||
const titleText = this.getTitle ? this.getTitle() : this.title;
|
||
if (titleText) {
|
||
ctx.save();
|
||
const fontSize = LiteGraph.NODE_TEXT_SIZE || 14;
|
||
const collW = this._collapsed_width || w;
|
||
const normalTitleColor = themeEditor?.config?.nodes?.titleColor || "#18181b";
|
||
// Respect bypass (mode 4) and mute (mode 2) states
|
||
const isBypassed = this.mode === 4;
|
||
const isMuted = this.mode === 2;
|
||
const titleColor = isBypassed ? "#4a2060" : isMuted ? "#2a2a2a" : normalTitleColor;
|
||
const textColor = isBypassed ? "#e8d5f5" : isMuted ? "#666666" : "#e4e4e7";
|
||
const r = 10; // match group border radius
|
||
const textX = 20;
|
||
|
||
// Force full opacity (LiteGraph reduces globalAlpha for bypass/mute)
|
||
const prevAlpha = ctx.globalAlpha;
|
||
ctx.globalAlpha = 1;
|
||
|
||
// 1. Cover entire collapsed pill (overwrite LiteGraph's shape + text)
|
||
// Extend 2px to fully cover LiteGraph's bypass/mute shape
|
||
ctx.fillStyle = titleColor;
|
||
ctx.beginPath();
|
||
ctx.roundRect(0, -titleH, collW, titleH, r);
|
||
ctx.fill();
|
||
|
||
// 2. Collapse dot — colored by connected input(s), white if none
|
||
const canvas = comfyApp.canvas;
|
||
const byType = canvas?.default_connection_color_byType || {};
|
||
const fallbackColor = canvas?.default_connection_color?.output_on || "#aaa";
|
||
const centerY = -titleH * 0.5;
|
||
const realInputs = this._realInputs || this.inputs || [];
|
||
const realOutputs = this._realOutputs || this.outputs || [];
|
||
|
||
const connectedInputs = realInputs.filter((input, index) =>
|
||
this.isInputConnected ? this.isInputConnected(index) : input.link != null);
|
||
let dotColor;
|
||
if (isBypassed) {
|
||
dotColor = "#c090e0";
|
||
} else if (isMuted) {
|
||
dotColor = "#555";
|
||
} else if (connectedInputs.length === 0) {
|
||
dotColor = "#e4e4e7";
|
||
} else if (connectedInputs.length === 1) {
|
||
dotColor = byType[connectedInputs[0].type] || fallbackColor;
|
||
} else {
|
||
// Blend connected input colors
|
||
let rr = 0, gg = 0, bb = 0;
|
||
connectedInputs.forEach(inp => {
|
||
const hex = (byType[inp.type] || fallbackColor).replace("#", "");
|
||
rr += parseInt(hex.substring(0, 2), 16);
|
||
gg += parseInt(hex.substring(2, 4), 16);
|
||
bb += parseInt(hex.substring(4, 6), 16);
|
||
});
|
||
const n = connectedInputs.length;
|
||
dotColor = `rgb(${Math.round(rr/n)},${Math.round(gg/n)},${Math.round(bb/n)})`;
|
||
}
|
||
ctx.beginPath();
|
||
ctx.arc(12, centerY, 5, 0, Math.PI * 2);
|
||
ctx.fillStyle = dotColor;
|
||
ctx.fill();
|
||
|
||
// 3. Draw full title text
|
||
ctx.font = `${fontSize}px Inter, Arial, sans-serif`;
|
||
ctx.fillStyle = textColor;
|
||
ctx.textAlign = "left";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText(titleText, textX, centerY);
|
||
|
||
// 4. Output: small rounded tick inside pill, right edge
|
||
realOutputs.forEach((out, index) => {
|
||
if (this.isOutputConnected ? this.isOutputConnected(index) : out.links?.length > 0) {
|
||
ctx.fillStyle = byType[out.type] || fallbackColor;
|
||
ctx.beginPath();
|
||
ctx.roundRect(collW - 6, centerY - 5, 3, 10, 1.5);
|
||
ctx.fill();
|
||
}
|
||
});
|
||
|
||
ctx.globalAlpha = prevAlpha;
|
||
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
// ── Execution glow ──
|
||
const nodeId = this.id;
|
||
const now = performance.now();
|
||
let glowColor = null;
|
||
let alpha = 0;
|
||
|
||
// Running: simple accent border (no pulse — saves setDirty calls)
|
||
if (executionState.runningNodeId == nodeId) {
|
||
alpha = 0.25;
|
||
glowColor = `rgba(168, 162, 255, ${alpha})`;
|
||
}
|
||
// Completed: brief green border (no fade animation — saves frames)
|
||
else if (executionState.completedNodes[nodeId]) {
|
||
const elapsed = now - executionState.completedNodes[nodeId];
|
||
if (elapsed < 400) {
|
||
alpha = 0.3;
|
||
glowColor = `rgba(34, 197, 94, ${alpha})`;
|
||
} else {
|
||
delete executionState.completedNodes[nodeId];
|
||
}
|
||
}
|
||
// Error: red border (no fade animation)
|
||
else if (executionState.errorNodes[nodeId]) {
|
||
const elapsed = now - executionState.errorNodes[nodeId];
|
||
if (elapsed < 800) {
|
||
alpha = 0.35;
|
||
glowColor = `rgba(239, 68, 68, ${alpha})`;
|
||
} else {
|
||
delete executionState.errorNodes[nodeId];
|
||
}
|
||
}
|
||
|
||
if (glowColor && alpha > 0.01) {
|
||
ctx.save();
|
||
ctx.strokeStyle = glowColor;
|
||
ctx.lineWidth = 2;
|
||
|
||
let gx, gy, gw, gh, gr;
|
||
if (this.flags?.collapsed) {
|
||
const collW = this._collapsed_width || w;
|
||
gx = 0;
|
||
gy = -titleH;
|
||
gw = collW;
|
||
gh = titleH;
|
||
gr = titleH * 0.5; // pill shape
|
||
} else {
|
||
gx = 0;
|
||
gy = -titleH;
|
||
gw = w;
|
||
gh = h + titleH;
|
||
gr = radius;
|
||
}
|
||
|
||
ctx.beginPath();
|
||
if (ctx.roundRect) {
|
||
ctx.roundRect(gx, gy, gw, gh, [gr]);
|
||
} else {
|
||
ctx.rect(gx, gy, gw, gh);
|
||
}
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
};
|
||
}
|
||
};
|
||
|
||
if (comfyApp?.registerExtension) {
|
||
comfyApp.registerExtension(LINEAR_THEME_EXTENSION);
|
||
} else {
|
||
// CSS is already injected above — fonts and UI styling still apply
|
||
console.warn("[LinearTheme] registerExtension unavailable — running in CSS-only mode");
|
||
}
|