Support live ComfyUI theme changes

This commit is contained in:
Jordan Thompson
2026-01-10 14:49:23 -08:00
parent 54b5370cd5
commit c486497e61
4 changed files with 247 additions and 2 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "ComfyUI_Viewer"
version = "0.3.0"
version = "0.3.1"
description = "Extensible content viewer for ComfyUI: HTML, Markdown (LaTeX, Mermaid, etc), Code, Images, and more"
readme = "README.md"
requires-python = ">=3.10"
+229 -1
View File
@@ -16,7 +16,7 @@ import {
isViewUI
} from "./views/view_loader.js";
import { loadPrismScripts } from "./views/code_scripts.js";
import { computeThemeTokens } from "./utils/theme.js";
import { computeThemeTokens, getFullTheme, themeToCssVars } from "./utils/theme.js";
import { createControlsBar, CONTROLS_HEIGHT, updateViewSelector, updateControlsForUI } from "./controls/controls_bar.js";
import { buildIframeContent, LIST_SEPARATOR } from "./iframe/iframe_builder.js";
@@ -36,8 +36,229 @@ const STATE = {
iframeLoadQueue: [],
iframeLoading: false,
hangerCheckIntervalId: null,
themeSyncAttached: false,
lastThemeCssVars: "",
themeUpdatePending: false,
};
function getThemeCssVars() {
try {
return themeToCssVars(getFullTheme());
} catch (e) {
return "";
}
}
function applyThemeToElements(theme, elements) {
if (!theme || !elements) return;
const bindHover = (btn, onEnter, onLeave) => {
if (!btn) return;
btn.onmouseenter = onEnter;
btn.onmouseleave = onLeave;
};
if (elements.controls) {
elements.controls.style.background = theme.bg;
elements.controls.style.borderBottomColor = theme.border;
}
if (elements.typeLabel) {
elements.typeLabel.style.color = theme.fg;
}
if (elements.viewSelector) {
elements.viewSelector.style.background = theme.bg;
elements.viewSelector.style.color = theme.fg;
elements.viewSelector.style.borderColor = theme.border;
}
if (elements.toggleAllBtn) {
elements.toggleAllBtn.style.background = theme.bg;
elements.toggleAllBtn.style.color = theme.fg;
elements.toggleAllBtn.style.borderColor = theme.border;
bindHover(
elements.toggleAllBtn,
() => {
elements.toggleAllBtn.style.background = theme.accent;
elements.toggleAllBtn.style.color = "#fff";
},
() => {
elements.toggleAllBtn.style.background = theme.bg;
elements.toggleAllBtn.style.color = theme.fg;
}
);
}
if (elements.editBtn) {
elements.editBtn.style.background = theme.bg;
elements.editBtn.style.color = theme.fg;
elements.editBtn.style.borderColor = theme.border;
bindHover(
elements.editBtn,
() => {
elements.editBtn.style.background = theme.accent;
elements.editBtn.style.color = "#fff";
},
() => {
elements.editBtn.style.background = theme.bg;
elements.editBtn.style.color = theme.fg;
}
);
}
if (elements.clearBtn) {
elements.clearBtn.style.background = theme.bg;
elements.clearBtn.style.color = theme.fg;
elements.clearBtn.style.borderColor = theme.border;
bindHover(
elements.clearBtn,
() => {
elements.clearBtn.style.background = "#c44";
elements.clearBtn.style.color = "#fff";
},
() => {
elements.clearBtn.style.background = theme.bg;
elements.clearBtn.style.color = theme.fg;
}
);
}
if (elements.fullscreenBtn) {
elements.fullscreenBtn.style.background = theme.bg;
elements.fullscreenBtn.style.color = theme.fg;
elements.fullscreenBtn.style.borderColor = theme.border;
bindHover(
elements.fullscreenBtn,
() => {
elements.fullscreenBtn.style.background = theme.accent;
elements.fullscreenBtn.style.color = "#fff";
},
() => {
elements.fullscreenBtn.style.background = theme.bg;
elements.fullscreenBtn.style.color = theme.fg;
}
);
}
if (elements.downloadBtn) {
elements.downloadBtn.style.background = theme.bg;
elements.downloadBtn.style.borderColor = theme.border;
const path = elements.downloadBtn.querySelector?.("path");
if (path) path.setAttribute("fill", theme.fg);
const updateDownloadSvg = (color) => {
const p = elements.downloadBtn.querySelector?.("path");
if (p) p.setAttribute("fill", color);
};
bindHover(
elements.downloadBtn,
() => {
elements.downloadBtn.style.background = theme.accent;
updateDownloadSvg("#fff");
},
() => {
elements.downloadBtn.style.background = theme.bg;
updateDownloadSvg(theme.fg);
}
);
}
if (elements.wrapper) {
elements.wrapper.style.background = theme.bg;
}
if (elements.iframe) {
elements.iframe.style.background = theme.bg;
}
if (elements.textarea) {
elements.textarea.style.background = theme.bg;
elements.textarea.style.color = theme.fg;
}
if (elements.mutedOverlay) {
elements.mutedOverlay.style.background = theme.bg;
}
if (elements.lowQualityOverlay) {
elements.lowQualityOverlay.style.background = theme.bg;
}
}
function broadcastThemeUpdate() {
const cssVars = getThemeCssVars();
if (!cssVars || cssVars === STATE.lastThemeCssVars) return;
STATE.lastThemeCssVars = cssVars;
const themeTokens = computeThemeTokens();
for (const elements of STATE.nodeIdToElements.values()) {
try {
applyThemeToElements(themeTokens, elements);
elements?.iframe?.contentWindow?.postMessage({
type: "was-theme-update",
cssVars,
}, "*");
} catch (e) {}
}
}
function scheduleThemeUpdate() {
if (STATE.themeUpdatePending) return;
STATE.themeUpdatePending = true;
window.requestAnimationFrame(() => {
STATE.themeUpdatePending = false;
broadcastThemeUpdate();
});
}
function ensureThemeSyncRunning() {
if (STATE.themeSyncAttached) return;
STATE.themeSyncAttached = true;
// Initialize baseline
STATE.lastThemeCssVars = getThemeCssVars();
const onThemeMaybeChanged = () => scheduleThemeUpdate();
// Observe attribute changes; ComfyUI theme switching often toggles classes/attributes
try {
const observer = new MutationObserver(onThemeMaybeChanged);
if (document.documentElement) {
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style", "data-theme", "data-color-scheme"],
});
}
if (document.body) {
observer.observe(document.body, {
attributes: true,
attributeFilter: ["class", "style", "data-theme", "data-color-scheme"],
});
}
} catch (e) {}
// Fallback: periodic check (covers cases where theme changes via stylesheet updates without attribute mutation)
window.setInterval(() => {
try {
const cssVars = getThemeCssVars();
if (cssVars && cssVars !== STATE.lastThemeCssVars) {
scheduleThemeUpdate();
}
} catch (e) {}
}, 1500);
// Also update on focus/visibility (covers theme change while tab is backgrounded)
window.addEventListener("focus", onThemeMaybeChanged);
window.addEventListener("visibilitychange", onThemeMaybeChanged);
}
function getBasePath() {
return import.meta.url.substring(0, import.meta.url.lastIndexOf("/"));
}
@@ -59,6 +280,8 @@ async function initializeViews() {
initializeViews();
ensureThemeSyncRunning();
onViewsRefresh(() => {
refreshAllViewers();
});
@@ -75,6 +298,11 @@ function refreshAllViewers() {
}
}
}
// Also push current theme to any existing iframes.
try {
broadcastThemeUpdate();
} catch (e) {}
}
function getActiveGraphNodes() {
+1
View File
@@ -217,6 +217,7 @@ export function createControlsBar(node, elements, callbacks) {
const fullscreenBtn = createFullscreenButton(theme, elements);
controls.appendChild(fullscreenBtn);
elements.fullscreenBtn = fullscreenBtn;
const downloadBtn = createDownloadButton(theme, node, elements, callbacks);
controls.appendChild(downloadBtn);
+16
View File
@@ -222,6 +222,22 @@ export function buildIframeContent(content, contentType, theme, excluded = [], n
window.WAS_NODE_ID = '${nodeId || ''}';
window.WAS_SCRIPTS_LOADED = false;
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'was-theme-update') {
try {
var cssVars = event.data.cssVars || '';
var styleEl = document.getElementById('was-theme-vars');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'was-theme-vars';
document.head.appendChild(styleEl);
}
styleEl.textContent = cssVars;
} catch (e) {
console.error('[WAS Viewer] Theme update error:', e);
}
return;
}
if (event.data && event.data.type === 'was-inject-scripts' && !window.WAS_SCRIPTS_LOADED) {
window.WAS_SCRIPTS_LOADED = true;
var scripts = event.data.scripts || [];