-
Reset
-
Preview
-
Apply & Close
-
Cancel
+
+
+ Apply
+ Preview
+ Reset
+ 💾 Save as Default
+ Cancel
`;
+
+ document.body.appendChild(dialog);
+ makeDraggable(dialog, dialog.querySelector('#drag-bar'));
+ setupDialogHandlers(dialog);
+ currentDialog = dialog;
- document.body.appendChild(backdrop);
- document.body.appendChild(dialog);
+ // Store the values when dialog opens for cancel functionality
+ dialogOpenValues = { ...currentValues };
+
+ // Set current values in the dialog
+ updateDialogValues(dialog);
- // ESC key handler
- document.addEventListener('keydown', function escHandler(e) {
- if (e.key === 'Escape') {
- backdrop.remove();
- dialog.remove();
- document.removeEventListener('keydown', escHandler);
- }
- });
-
- // Set up event handlers
- setupDialogHandlers(dialog, backdrop);
+ // Live theme updating without closing dialog
+ unregisterThemeCallback = onThemeChange(() => {
+ if (currentDialog) {
+ updateDialogTheme();
+ }
+ });
}
-
- function setupDialogHandlers(dialog, backdrop) {
-// call drag function
+ function createStyleCSS(colors) {
+ return `
+ .fontifier-setting {
+ margin-bottom: 10px;
+ }
+ .fontifier-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+ .fontifier-row input[type="range"] {
+ flex-grow: 1;
+ }
+ .fontifier-row input[type="number"] {
+ width: 50px;
+ background: ${colors.inputBg || '#222'};
+ border: 1px solid ${colors.border || '#999'};
+ color: ${colors.inputText || '#ddd'};
+ border-radius: 4px;
+ padding: 2px;
+ }
+ #fontifier-dialog button {
+ flex-grow: 1;
+ padding: 6px;
+ background: ${colors.inputBg || '#222'};
+ border: 1px solid ${colors.border || '#999'};
+ color: ${colors.inputText || '#ddd'};
+ border-radius: 4px;
+ cursor: pointer;
+ }
+ #fontifier-dialog button:hover {
+ background: ${colors.buttonHoverBg || colors.hoverBg || blendColors(colors.inputBg || '#222', '#ffffff', 0.1)};
+ }
+ #fontifier-dialog #apply-btn {
+ background: ${toRGBA(colors.accent || '#4CAF50', 0.3)};
+ border-color: ${colors.accent || '#4CAF50'};
+ }
+ #fontifier-dialog #cancel-btn {
+ background: ${toRGBA(colors.errorText || '#f44336', 0.3)};
+ border-color: ${colors.errorText || '#f44336'};
+ }
+ #fontifier-dialog select {
+ background: ${colors.inputBg || '#222'};
+ border: 1px solid ${colors.border || '#999'};
+ color: ${colors.inputText || '#ddd'};
+ border-radius: 4px;
+ padding: 4px;
+ }
+ `;
+ }
- makeDraggable(dialog);
+ function updateDialogTheme() {
+ if (!currentDialog) return;
+
+ const newColors = getComfyUIColors();
+
+ // Update main dialog styling
+ currentDialog.style.background = newColors.dialogBg || newColors.menu || 'rgba(20, 20, 20, 0.95)';
+ currentDialog.style.color = newColors.inputText || '#fff';
+ currentDialog.style.borderColor = newColors.border || '#999';
+ currentDialog.style.boxShadow = newColors.shadow || '0 0 20px rgba(0,0,0,0.5)';
+
+ // Update drag bar
+ const dragBar = currentDialog.querySelector('#drag-bar');
+ if (dragBar) {
+ dragBar.style.background = newColors.menuSecondary || '#2a2a2a';
+ }
+
+ // Update preview indicator
+ const previewIndicator = currentDialog.querySelector('#preview-indicator');
+ if (previewIndicator) {
+ previewIndicator.style.color = newColors.accent || '#4CAF50';
+ }
+
+ // Update the style tag with new colors
+ const styleTag = document.getElementById('fontifier-dialog-style');
+ if (styleTag) {
+ styleTag.textContent = createStyleCSS(newColors);
+ }
+ }
+
+ function updateDialogValues(dialog) {
+ dialog.querySelector('#global-scale').value = currentValues.GLOBAL_SCALE || 1;
+ dialog.querySelector('#global-scale-num').value = currentValues.GLOBAL_SCALE || 1;
+ dialog.querySelector('#node-text-size').value = currentValues.NODE_TEXT_SIZE;
+ dialog.querySelector('#node-text-size-num').value = currentValues.NODE_TEXT_SIZE;
+ dialog.querySelector('#node-subtext-size').value = currentValues.NODE_SUBTEXT_SIZE;
+ dialog.querySelector('#node-subtext-size-num').value = currentValues.NODE_SUBTEXT_SIZE;
+ dialog.querySelector('#title-height').value = currentValues.NODE_TITLE_HEIGHT;
+ dialog.querySelector('#title-height-num').value = currentValues.NODE_TITLE_HEIGHT;
+ dialog.querySelector('#slot-height').value = currentValues.NODE_SLOT_HEIGHT;
+ dialog.querySelector('#slot-height-num').value = currentValues.NODE_SLOT_HEIGHT;
+ dialog.querySelector('#group-font-size').value = currentValues.DEFAULT_GROUP_FONT;
+ dialog.querySelector('#group-font-size-num').value = currentValues.DEFAULT_GROUP_FONT;
+ dialog.querySelector('#widget-text-size').value = currentValues.WIDGET_TEXT_SIZE;
+ dialog.querySelector('#widget-text-size-num').value = currentValues.WIDGET_TEXT_SIZE;
+ dialog.querySelector('#font-family').value = currentValues.NODE_FONT;
+ }
+
+ function setupDialogHandlers(dialog) {
+ if (handlersSetup) return;
+ handlersSetup = true;
+
+ addButtonHoverEffects(dialog);
- // Sync sliders with number inputs
const elements = [
'global-scale',
- 'node-text-size',
- 'node-subtext-size',
- 'title-height',
+ 'node-text-size',
+ 'node-subtext-size',
+ 'title-height',
'slot-height',
'group-font-size',
'widget-text-size'
];
-
+
elements.forEach(id => {
const slider = dialog.querySelector(`#${id}`);
const numberInput = dialog.querySelector(`#${id}-num`);
-
- slider.oninput = () => {
- numberInput.value = slider.value;
- // Update global scale number input properly
- if (id === 'global-scale') {
- const globalScaleNum = dialog.querySelector('#global-scale-num');
- globalScaleNum.value = slider.value;
- }
- };
- numberInput.oninput = () => {
- slider.value = numberInput.value;
- };
+ if (slider && numberInput) {
+ slider.oninput = () => {
+ numberInput.value = slider.value;
+ if (isPreviewMode) showPreviewIndicator();
+ };
+ numberInput.oninput = () => {
+ // Enforce min/max constraints
+ const min = parseFloat(numberInput.min);
+ const max = parseFloat(numberInput.max);
+ let value = parseFloat(numberInput.value);
+ if (value < min) value = min;
+ if (value > max) value = max;
+ numberInput.value = value;
+ slider.value = value;
+ if (isPreviewMode) showPreviewIndicator();
+ };
+ }
});
-
- // Global scale handler
- const globalScale = dialog.querySelector('#global-scale');
- const globalScaleNum = dialog.querySelector('#global-scale-num');
-
- function updateGlobalScale() {
- const scale = parseFloat(globalScale.value);
- globalScaleNum.value = scale; // Fix: Update the number input
-
- // Update all individual controls
- const updates = [
- ['node-text-size', originalValues.NODE_TEXT_SIZE],
- ['node-subtext-size', originalValues.NODE_SUBTEXT_SIZE],
- ['title-height', originalValues.NODE_TITLE_HEIGHT],
- ['slot-height', originalValues.NODE_SLOT_HEIGHT],
- ['group-font-size', originalValues.DEFAULT_GROUP_FONT]
- ];
-
- updates.forEach(([id, originalValue]) => {
- const newValue = Math.round(originalValue * scale);
- dialog.querySelector(`#${id}`).value = newValue;
- dialog.querySelector(`#${id}-num`).value = newValue;
- });
+
+ const saveBtn = dialog.querySelector('#save-defaults-btn');
+ if (saveBtn) {
+ saveBtn.onclick = () => {
+ localStorage.setItem("endless_fontifier_defaults", JSON.stringify(currentValues));
+ alert("🌊 Fontifier defaults saved! They'll auto-load next time.");
+ };
}
-
- globalScale.oninput = updateGlobalScale;
- globalScaleNum.oninput = () => {
- globalScale.value = globalScaleNum.value;
- updateGlobalScale();
+
+ dialog.querySelector('#apply-btn').onclick = () => {
+ applyChanges(dialog, true);
+ hidePreviewIndicator();
+ closeDialog();
};
- // Button handlers
- dialog.querySelector('#close-dialog').onclick = () => {
- backdrop.remove();
- dialog.remove();
+ dialog.querySelector('#preview-btn').onclick = () => {
+ applyChanges(dialog, false);
+ showPreviewIndicator();
};
dialog.querySelector('#reset-btn').onclick = () => {
- dialog.querySelector('#global-scale').value = 1;
- dialog.querySelector('#global-scale-num').value = 1;
- dialog.querySelector('#node-text-size').value = originalValues.NODE_TEXT_SIZE;
- dialog.querySelector('#node-text-size-num').value = originalValues.NODE_TEXT_SIZE;
- dialog.querySelector('#node-subtext-size').value = originalValues.NODE_SUBTEXT_SIZE;
- dialog.querySelector('#node-subtext-size-num').value = originalValues.NODE_SUBTEXT_SIZE;
- dialog.querySelector('#title-height').value = originalValues.NODE_TITLE_HEIGHT;
- dialog.querySelector('#title-height-num').value = originalValues.NODE_TITLE_HEIGHT;
- dialog.querySelector('#slot-height').value = originalValues.NODE_SLOT_HEIGHT;
- dialog.querySelector('#slot-height-num').value = originalValues.NODE_SLOT_HEIGHT;
- dialog.querySelector('#group-font-size').value = originalValues.DEFAULT_GROUP_FONT;
- dialog.querySelector('#group-font-size-num').value = originalValues.DEFAULT_GROUP_FONT;
- dialog.querySelector('#font-family').value = 'Arial';
- };
-
- dialog.querySelector('#preview-btn').onclick = () => applyChanges(dialog, false);
-
- dialog.querySelector('#apply-btn').onclick = () => {
- applyChanges(dialog, true);
- backdrop.remove();
- dialog.remove();
+ localStorage.removeItem("endless_fontifier_defaults");
+ currentValues = { ...originalValues };
+ applySettingsToComfyUI(originalValues);
+ updateDialogValues(dialog);
+ hidePreviewIndicator();
+ alert("🔁 Fontifier reset to ComfyUI defaults.");
};
dialog.querySelector('#cancel-btn').onclick = () => {
- backdrop.remove();
- dialog.remove();
+ applySettingsToComfyUI(dialogOpenValues);
+ hidePreviewIndicator();
+ closeDialog();
};
- // Add hover effects to buttons
- const buttons = dialog.querySelectorAll('button');
- buttons.forEach(button => {
- button.style.boxSizing = 'border-box';
- button.style.minWidth = button.offsetWidth + 'px'; // Lock the width
- button.addEventListener('mouseenter', () => {
- button.style.borderWidth = '2px';
- button.style.padding = '7px 15px';
- });
- button.addEventListener('mouseleave', () => {
- button.style.borderWidth = '1px';
- button.style.padding = '8px 16px';
- });
- });
+ escHandler = e => {
+ if (e.key === 'Escape') {
+ applySettingsToComfyUI(dialogOpenValues);
+ hidePreviewIndicator();
+ closeDialog();
+ }
+ };
+ document.addEventListener('keydown', escHandler);
}
-
+
+ function showPreviewIndicator() {
+ if (!currentDialog) return;
+ isPreviewMode = true;
+ const indicator = currentDialog.querySelector('#preview-indicator');
+ if (indicator) indicator.style.display = 'block';
+ }
+
+ function hidePreviewIndicator() {
+ if (!currentDialog) return;
+ isPreviewMode = false;
+ const indicator = currentDialog.querySelector('#preview-indicator');
+ if (indicator) indicator.style.display = 'none';
+ }
+
function applyChanges(dialog, permanent = false) {
- const newValues = {
+ const globalScale = parseFloat(dialog.querySelector('#global-scale').value);
+
+ const baseValues = {
NODE_TEXT_SIZE: parseInt(dialog.querySelector('#node-text-size').value),
NODE_SUBTEXT_SIZE: parseInt(dialog.querySelector('#node-subtext-size').value),
NODE_TITLE_HEIGHT: parseInt(dialog.querySelector('#title-height').value),
NODE_SLOT_HEIGHT: parseInt(dialog.querySelector('#slot-height').value),
DEFAULT_GROUP_FONT: parseInt(dialog.querySelector('#group-font-size').value),
- FONT_FAMILY: dialog.querySelector('#font-family').value
+ FONT_FAMILY: dialog.querySelector('#font-family').value,
+ NODE_FONT: dialog.querySelector('#font-family').value,
+ WIDGET_TEXT_SIZE: parseInt(dialog.querySelector('#widget-text-size').value),
+ GLOBAL_SCALE: globalScale
};
- if (typeof LiteGraph !== 'undefined') {
- LiteGraph.NODE_TEXT_SIZE = newValues.NODE_TEXT_SIZE;
- LiteGraph.NODE_SUBTEXT_SIZE = newValues.NODE_SUBTEXT_SIZE;
- LiteGraph.NODE_TITLE_HEIGHT = newValues.NODE_TITLE_HEIGHT;
- LiteGraph.NODE_SLOT_HEIGHT = newValues.NODE_SLOT_HEIGHT;
- LiteGraph.NODE_WIDGET_HEIGHT = newValues.NODE_SLOT_HEIGHT;
- LiteGraph.DEFAULT_GROUP_FONT = newValues.DEFAULT_GROUP_FONT;
- LiteGraph.DEFAULT_GROUP_FONT_SIZE = newValues.DEFAULT_GROUP_FONT;
- LiteGraph.NODE_FONT = newValues.FONT_FAMILY;
- LiteGraph.DEFAULT_FONT = newValues.FONT_FAMILY;
- LiteGraph.GROUP_FONT = newValues.FONT_FAMILY;
+ // Apply global scaling to font sizes
+ const scaledValues = {
+ ...baseValues,
+ NODE_TEXT_SIZE: Math.round(baseValues.NODE_TEXT_SIZE * globalScale),
+ NODE_SUBTEXT_SIZE: Math.round(baseValues.NODE_SUBTEXT_SIZE * globalScale),
+ DEFAULT_GROUP_FONT: Math.round(baseValues.DEFAULT_GROUP_FONT * globalScale),
+ WIDGET_TEXT_SIZE: Math.round(baseValues.WIDGET_TEXT_SIZE * globalScale)
+ };
- console.log('🌊✨ Fontifier applied:', newValues);
+ applySettingsToComfyUI(scaledValues);
+ if (permanent) {
+ currentValues = { ...baseValues }; // Store unscaled values
+ isPreviewMode = false;
+ }
+ }
- if (typeof app !== 'undefined' && app.canvas) {
- app.canvas.setDirty(true, true);
- if (app.canvas.draw) {
- setTimeout(() => app.canvas.draw(true, true), 100);
- }
- }
+ function applySettingsToComfyUI(settings) {
+ LiteGraph.NODE_TEXT_SIZE = settings.NODE_TEXT_SIZE;
+ LiteGraph.NODE_SUBTEXT_SIZE = settings.NODE_SUBTEXT_SIZE;
+ LiteGraph.NODE_TITLE_HEIGHT = settings.NODE_TITLE_HEIGHT;
+ LiteGraph.NODE_SLOT_HEIGHT = settings.NODE_SLOT_HEIGHT;
+ LiteGraph.DEFAULT_GROUP_FONT = settings.DEFAULT_GROUP_FONT;
+ LiteGraph.DEFAULT_GROUP_FONT_SIZE = settings.DEFAULT_GROUP_FONT;
+ LiteGraph.NODE_FONT = settings.NODE_FONT;
+ LiteGraph.DEFAULT_FONT = settings.NODE_FONT;
+ LiteGraph.GROUP_FONT = settings.NODE_FONT;
- const canvases = document.querySelectorAll('canvas');
- canvases.forEach(canvas => {
- if (canvas.getContext) {
- const ctx = canvas.getContext('2d');
- const originalWidth = canvas.width;
- canvas.width = originalWidth + 1;
- canvas.width = originalWidth;
- }
- });
+ if (window.app?.canvas) {
+ window.app.canvas.setDirty(true, true);
+ setTimeout(() => window.app.canvas.draw(true, true), 100);
}
- // Apply widget font size to CSS, this is DOM-only
- const widgetTextSize = parseInt(dialog.querySelector('#widget-text-size').value);
- let styleTag = document.getElementById('fontifier-widget-text-style');
+ const styleId = "fontifier-widget-text-style";
+ let styleTag = document.getElementById(styleId);
if (!styleTag) {
styleTag = document.createElement('style');
- styleTag.id = 'fontifier-widget-text-style';
+ styleTag.id = styleId;
document.head.appendChild(styleTag);
}
styleTag.textContent = `
- canvas ~ * .widget input, canvas ~ * .widget select, canvas ~ * .widget textarea,
- canvas ~ * .comfy-multiline-input, canvas ~ * .comfy-input,
- canvas ~ * input.comfy-multiline-input, canvas ~ * textarea.comfy-multiline-input,
- canvas ~ * [class*="comfy-input"], canvas ~ * [class*="comfy-multiline"],
- canvas ~ * .comfyui-widget input, canvas ~ * .comfyui-widget select, canvas ~ * .comfyui-widget textarea,
- canvas ~ * [class*="widget"] input, canvas ~ * [class*="widget"] select, canvas ~ * [class*="widget"] textarea,
- canvas ~ * .litegraph input, canvas ~ * .litegraph select, canvas ~ * .litegraph textarea,
.litegraph input, .litegraph select, .litegraph textarea {
- font-size: ${widgetTextSize}px !important;
- font-family: ${newValues.FONT_FAMILY} !important;
+ font-size: ${settings.WIDGET_TEXT_SIZE}px !important;
+ font-family: ${settings.NODE_FONT} !important;
}
-
- /* Exclude the fontifier dialog itself */
#fontifier-dialog input, #fontifier-dialog select, #fontifier-dialog textarea {
font-size: 14px !important;
font-family: Arial !important;
}
`;
-
- if (permanent) {
- currentValues = { ...newValues };
- console.log('🌊✨ Fontifier changes applied permanently (until page refresh)');
- }
}
-
- function findToolbar() {
- // Method 1: Look for ComfyUI specific toolbar classes
- let toolbar = document.querySelector('.comfyui-menu, .comfy-menu, [class*="menu"], [class*="toolbar"]');
-
- // Method 2: Look for button groups
- if (!toolbar) {
- const buttonGroups = document.querySelectorAll('[class*="button-group"], [class*="btn-group"], .comfyui-button-group');
- toolbar = Array.from(buttonGroups).find(group =>
- group.querySelectorAll('button').length > 0
- );
+ function closeDialog() {
+ if (currentDialog) currentDialog.remove();
+ if (escHandler) document.removeEventListener('keydown', escHandler);
+ if (unregisterThemeCallback) unregisterThemeCallback();
+ // Clean up the style tag
+ const styleTag = document.getElementById('fontifier-dialog-style');
+ if (styleTag) styleTag.remove();
+ currentDialog = null;
+ handlersSetup = false;
+ escHandler = null;
+ unregisterThemeCallback = null;
+ isPreviewMode = false;
+ }
+
+ // Wait for app to be ready before initializing
+ function waitForApp() {
+ if (typeof window.app !== 'undefined' && window.app?.canvas) {
+ // Initialize with saved defaults once app is ready
+ applySettingsToComfyUI(currentValues);
+ return;
}
-
- // Method 3: Look for any container with multiple buttons
- if (!toolbar) {
- const allElements = document.querySelectorAll('*');
- toolbar = Array.from(allElements).find(el => {
- const buttons = el.querySelectorAll('button');
- return buttons.length >= 2 && buttons.length <= 10; // Reasonable toolbar size
- });
- }
-
- // Method 4: Fallback to the original Share button method
- if (!toolbar) {
- toolbar = Array.from(document.querySelectorAll(".comfyui-button-group")).find(div =>
- Array.from(div.querySelectorAll("button")).some(btn => btn.title === "Share")
- );
- }
-
- return toolbar;
+ setTimeout(waitForApp, 100);
}
- function injectFontifierButton() {
- const toolbar = findToolbar();
-
- if (toolbar && !document.getElementById("endless-fontifier-button")) {
- const colors = getComfyUIColors();
-
- const btn = document.createElement("button");
- btn.id = "endless-fontifier-button";
- btn.textContent = "🌊✨ Fontifier";
- btn.className = "comfyui-button";
+ waitForApp();
- // Function to update button colors
- function updateButtonColors() {
- const currentColors = getComfyUIColors();
- btn.style.cssText = `
- margin-left: 8px;
- background: ${currentColors.backgroundSecondary};
- border: 1px solid ${currentColors.border};
- color: ${currentColors.text};
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s ease;
- `;
-
- btn.onmouseover = () => {
- const hoverColors = getComfyUIColors();
- btn.style.background = hoverColors.background;
- btn.style.borderColor = hoverColors.text;
- };
-
- btn.onmouseout = () => {
- const outColors = getComfyUIColors();
- btn.style.background = outColors.backgroundSecondary;
- btn.style.borderColor = outColors.border;
- };
- }
-
- // Initial colors
- updateButtonColors();
-
- // Watch for theme changes
- const observer = new MutationObserver(updateButtonColors);
- observer.observe(document.documentElement, {
- attributes: true,
- attributeFilter: ['class', 'style']
- });
-
- btn.onclick = createFontifierDialog;
- toolbar.appendChild(btn);
-
- console.log("✅ 🌊✨ Endless Fontifier button injected successfully!");
- return true;
- }
- return false;
- }
-
- // Try to inject immediately
- if (!injectFontifierButton()) {
- // If immediate injection fails, use observer
- const observer = new MutationObserver(() => {
- if (injectFontifierButton()) {
- observer.disconnect();
- }
- });
-
- observer.observe(document.body, { childList: true, subtree: true });
-
- // Timeout after 30 seconds to avoid infinite observation
- setTimeout(() => {
- observer.disconnect();
- if (!document.getElementById("endless-fontifier-button")) {
- console.warn("⚠️ Could not find suitable toolbar for Fontifier button");
- }
- }, 30000);
- }
+ // Register into Endless Tools menu
+ registerEndlessTool("Fontifier", createFontifierDialog);
})();
\ No newline at end of file
diff --git a/web/endless_minimap.js b/web/endless_minimap.js
new file mode 100644
index 0000000..884f03f
--- /dev/null
+++ b/web/endless_minimap.js
@@ -0,0 +1,764 @@
+// ComfyUI Endless 🌊✨ Minimap - Optimized Version
+
+(function waitForHelpers() {
+ if (typeof window.EndlessHelpers === 'undefined') {
+ console.warn("⏳ Waiting for EndlessHelpers to be ready...");
+ setTimeout(waitForHelpers, 100);
+ return;
+ }
+
+ const {
+ registerEndlessTool,
+ onThemeChange,
+ getComfyUIColors,
+ toRGBA,
+ makeDraggable
+ } = window.EndlessHelpers;
+
+ console.log("✅ Endless Minimap loaded.");
+
+ // State variables
+ let currentDialog = null;
+ let animationId = null;
+ let unregisterThemeCallback = null;
+ let resizeObserver = null;
+
+ // Canvas state
+ let panX = 0, panY = 0, zoom = 1;
+ let isDragging = false;
+ let dragStartTime = 0, dragStartX = 0, dragStartY = 0;
+ const DRAG_THRESHOLD = 5;
+
+ // Size constants
+ const BASE_WIDTH = 300, BASE_HEIGHT = 400;
+ const MAX_WIDTH = 500, MAX_HEIGHT = 600;
+ const MIN_WIDTH = 200, MIN_HEIGHT = 150;
+
+ // Node type colors - comprehensive mapping
+ const NODE_COLORS = {
+ // Image Processing (Blue family)
+ 'LoadImage': '#5DADE2', 'SaveImage': '#3498DB', 'PreviewImage': '#2E86AB',
+ 'ImageScale': '#85C1E9', 'ImageCrop': '#7FB3D3', 'ImageBlend': '#6BB6FF',
+
+ // Latent Processing (Purple family)
+ 'KSampler': '#8E44AD', 'KSamplerAdvanced': '#9B59B6', 'EmptyLatentImage': '#A569BD',
+ 'LatentUpscale': '#BB8FCE', 'LatentBlend': '#D2B4DE',
+
+ // VAE (Green family)
+ 'VAEDecode': '#27AE60', 'VAEEncode': '#2ECC71', 'VAELoader': '#58D68D',
+
+ // Model/Checkpoint (Teal family)
+ 'CheckpointLoaderSimple': '#17A2B8', 'CheckpointLoader': '#148A99',
+ 'ModelMergeSimple': '#1ABC9C', 'UNETLoader': '#5DADE2',
+
+ // CLIP/Text (Orange family)
+ 'CLIPTextEncode': '#E67E22', 'CLIPTextEncodeSDXL': '#F39C12',
+ 'CLIPLoader': '#F8C471', 'CLIPVisionEncode': '#D68910',
+
+ // LoRA (Yellow family)
+ 'LoraLoader': '#F1C40F', 'LoraLoaderModelOnly': '#F4D03F',
+
+ // ControlNet (Pink family)
+ 'ControlNetLoader': '#E91E63', 'ControlNetApply': '#F06292',
+ 'CannyEdgePreprocessor': '#EC407A', 'OpenposePreprocessor': '#F8BBD9',
+
+ // Conditioning (Coral family)
+ 'ConditioningAverage': '#FF6B35', 'ConditioningCombine': '#FF8C42',
+
+ // Utility (Gray family)
+ 'PrimitiveNode': '#95A5A6', 'Note': '#BDC3C7', 'Reroute': '#85929E',
+
+ // Upscaling (Lime family)
+ 'UpscaleModelLoader': '#7FB069', 'ImageUpscaleWithModel': '#8BC34A',
+
+ // Masks (Red family)
+ 'MaskComposite': '#E53935', 'MaskToImage': '#F44336', 'ImageToMask': '#EF5350',
+
+ 'default': 'rgba(200, 200, 200, 0.7)'
+ };
+
+ function getApp() {
+ return window.app || window.comfyApp || document.querySelector('#app')?.__vue__?.$root || null;
+ }
+
+ function getCanvasAspectRatio() {
+ const mainCanvas = document.querySelector('canvas') ||
+ document.querySelector('#graph-canvas') ||
+ document.querySelector('.litegraph');
+
+ if (mainCanvas) {
+ const rect = mainCanvas.getBoundingClientRect();
+ return rect.width / rect.height;
+ }
+
+ return window.innerWidth / window.innerHeight;
+ }
+
+ function calculateDimensions() {
+ const aspectRatio = getCanvasAspectRatio();
+ let containerWidth, containerHeight, canvasWidth, canvasHeight;
+
+ if (aspectRatio > 1) {
+ containerWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, BASE_WIDTH * aspectRatio));
+ containerHeight = BASE_HEIGHT;
+ canvasWidth = containerWidth;
+ canvasHeight = BASE_HEIGHT - 50;
+ } else {
+ containerWidth = BASE_WIDTH;
+ containerHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, BASE_HEIGHT / aspectRatio));
+ canvasWidth = containerWidth;
+ canvasHeight = containerHeight - 50;
+ }
+
+ return { containerWidth, containerHeight, canvasWidth, canvasHeight };
+ }
+
+ function getNodeColor(node) {
+ const nodeType = node.type || node.constructor?.name || 'default';
+
+ if (NODE_COLORS[nodeType]) return NODE_COLORS[nodeType];
+
+ // Pattern matching for common types
+ const patterns = [
+ ['Sampler', NODE_COLORS['KSampler']],
+ ['CLIP', NODE_COLORS['CLIPTextEncode']],
+ ['VAE', NODE_COLORS['VAEDecode']],
+ ['ControlNet', NODE_COLORS['ControlNetLoader']],
+ ['Lora', NODE_COLORS['LoraLoader']],
+ ['Image.*Load', NODE_COLORS['LoadImage']],
+ ['Image.*Save', NODE_COLORS['SaveImage']],
+ ['Checkpoint', NODE_COLORS['CheckpointLoaderSimple']],
+ ['Upscale', NODE_COLORS['UpscaleModelLoader']],
+ ['Mask', NODE_COLORS['MaskComposite']]
+ ];
+
+ for (const [pattern, color] of patterns) {
+ if (new RegExp(pattern, 'i').test(nodeType)) return color;
+ }
+
+ return NODE_COLORS.default;
+ }
+
+ function getNodes() {
+ const app = getApp();
+ if (!app) {
+ console.log("App not found, trying DOM fallback...");
+ const nodeElements = document.querySelectorAll('[class*="node"], .comfy-node, .litegraph-node');
+ if (nodeElements.length > 0) {
+ return Array.from(nodeElements).map((el, i) => ({
+ pos: [i * 150, i * 100],
+ size: [100, 60],
+ type: 'Unknown',
+ title: `Node ${i + 1}`
+ }));
+ }
+ return null;
+ }
+
+ const nodes = app.graph?._nodes ||
+ app.graph?.nodes ||
+ app.canvas?.graph?._nodes ||
+ app.canvas?.graph?.nodes ||
+ [];
+
+ return nodes;
+ }
+
+ function createStyleCSS(colors) {
+ return `
+ #endless-minimap button {
+ background: none;
+ border: none;
+ color: ${colors.inputText};
+ cursor: pointer;
+ padding: 2px 6px;
+ font-size: 18px;
+ border-radius: 3px;
+ transition: background 0.2s ease;
+ }
+ #endless-minimap button:hover {
+ background: ${toRGBA(colors.inputText, 0.1)};
+ }
+ #endless-minimap .drag-bar {
+ padding: 4px 8px;
+ background: ${toRGBA(colors.inputText, 0.05)};
+ cursor: move;
+ font-size: 14px;
+ user-select: none;
+ border-bottom: 1px solid ${colors.border};
+ flex-shrink: 0;
+ }
+ #endless-minimap .legend {
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ background: ${colors.menu};
+ color: ${colors.inputText};
+ padding: 8px;
+ border: 1px solid ${colors.border};
+ border-radius: 4px;
+ font-size: 10px;
+ max-height: 200px;
+ overflow-y: auto;
+ display: none;
+ z-index: 1;
+ }
+ #endless-minimap .pan-info {
+ padding: 2px 8px;
+ font-size: 10px;
+ background: ${colors.menuSecondary};
+ color: ${colors.inputText};
+ border-top: 1px solid ${colors.border};
+ text-align: center;
+ flex-shrink: 0;
+ }
+ `;
+ }
+
+ function updateTheme() {
+ if (!currentDialog) return;
+
+ const colors = getComfyUIColors();
+
+ // Update container colors only, not size
+ currentDialog.style.background = colors.menu;
+ currentDialog.style.color = colors.inputText;
+ currentDialog.style.borderColor = colors.accent;
+
+ // Update style tag
+ const styleTag = document.getElementById('minimap-style');
+ if (styleTag) {
+ styleTag.textContent = createStyleCSS(colors);
+ }
+
+ drawMinimap();
+ }
+
+ function updateLegend() {
+ const legend = currentDialog.querySelector('.legend');
+ if (!legend || legend.style.display === 'none') return;
+
+ const nodes = getNodes();
+ if (!nodes) return;
+
+ const typeCounts = {};
+ nodes.forEach(n => {
+ const nodeType = n.type || n.constructor?.name || 'default';
+ typeCounts[nodeType] = (typeCounts[nodeType] || 0) + 1;
+ });
+
+ legend.innerHTML = Object.entries(typeCounts)
+ .sort((a, b) => b[1] - a[1])
+ .map(([type, count]) => {
+ const color = NODE_COLORS[type] || NODE_COLORS.default;
+ return `
`;
+ }).join('');
+ }
+
+ // Get current transform state for coordinate conversions
+ function getTransformState() {
+ const canvas = currentDialog?.querySelector('canvas');
+ if (!canvas) return null;
+
+ const nodes = getNodes();
+ if (!nodes?.length) return null;
+
+ // Calculate bounds (same as in drawMinimap)
+ const bounds = nodes.reduce((acc, n) => {
+ const x = n.pos?.[0] ?? n.x ?? 0;
+ const y = n.pos?.[1] ?? n.y ?? 0;
+ const w = n.size?.[0] ?? n.width ?? 100;
+ const h = n.size?.[1] ?? n.height ?? 60;
+
+ return {
+ minX: Math.min(acc.minX, x),
+ minY: Math.min(acc.minY, y),
+ maxX: Math.max(acc.maxX, x + w),
+ maxY: Math.max(acc.maxY, y + h)
+ };
+ }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
+
+ const width = bounds.maxX - bounds.minX;
+ const height = bounds.maxY - bounds.minY;
+ if (width <= 0 || height <= 0) return null;
+
+ const baseScale = Math.min(canvas.width / Math.max(width, 1000), canvas.height / Math.max(height, 1000));
+ const scale = baseScale * zoom;
+
+ return {
+ bounds,
+ width,
+ height,
+ scale,
+ canvas
+ };
+ }
+
+ // Convert canvas coordinates to world coordinates
+ function canvasToWorld(canvasX, canvasY) {
+ const transform = getTransformState();
+ if (!transform) return null;
+
+ const { bounds, width, height, scale, canvas } = transform;
+
+ // Inverse of the transform used in drawMinimap
+ const worldX = (canvasX - canvas.width / 2 - panX) / scale + (bounds.minX + width / 2);
+ const worldY = (canvasY - canvas.height / 2 - panY) / scale + (bounds.minY + height / 2);
+
+ return { x: worldX, y: worldY };
+ }
+
+ // Convert world coordinates to canvas coordinates
+ function worldToCanvas(worldX, worldY) {
+ const transform = getTransformState();
+ if (!transform) return null;
+
+ const { bounds, width, height, scale, canvas } = transform;
+
+ // Same transform as used in drawMinimap
+ const canvasX = (worldX - (bounds.minX + width / 2)) * scale + canvas.width / 2 + panX;
+ const canvasY = (worldY - (bounds.minY + height / 2)) * scale + canvas.height / 2 + panY;
+
+ return { x: canvasX, y: canvasY };
+ }
+
+ function drawMinimap() {
+ if (!currentDialog) return;
+
+ const canvas = currentDialog.querySelector('canvas');
+ const panInfo = currentDialog.querySelector('.pan-info');
+ if (!canvas || !panInfo) return;
+
+ const ctx = canvas.getContext('2d');
+ const colors = getComfyUIColors();
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ const nodes = getNodes();
+ if (!nodes || !nodes.length) {
+ ctx.fillStyle = colors.inputText;
+ ctx.font = '12px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('No nodes found', canvas.width / 2, canvas.height / 2);
+ ctx.fillText('or graph not loaded', canvas.width / 2, canvas.height / 2 + 15);
+ return;
+ }
+
+ // Calculate bounds
+ const bounds = nodes.reduce((acc, n) => {
+ const x = n.pos?.[0] ?? n.x ?? 0;
+ const y = n.pos?.[1] ?? n.y ?? 0;
+ const w = n.size?.[0] ?? n.width ?? 100;
+ const h = n.size?.[1] ?? n.height ?? 60;
+
+ return {
+ minX: Math.min(acc.minX, x),
+ minY: Math.min(acc.minY, y),
+ maxX: Math.max(acc.maxX, x + w),
+ maxY: Math.max(acc.maxY, y + h)
+ };
+ }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
+
+ const width = bounds.maxX - bounds.minX;
+ const height = bounds.maxY - bounds.minY;
+ if (width <= 0 || height <= 0) return;
+
+ const baseScale = Math.min(canvas.width / Math.max(width, 1000), canvas.height / Math.max(height, 1000));
+ const scale = baseScale * zoom;
+
+ ctx.save();
+ ctx.translate(canvas.width / 2 + panX, canvas.height / 2 + panY);
+ ctx.scale(scale, scale);
+ ctx.translate(-(bounds.minX + width/2), -(bounds.minY + height/2));
+
+ // Draw grid
+ ctx.strokeStyle = toRGBA(colors.inputText, 0.1);
+ ctx.lineWidth = 1 / scale;
+ const gridSize = 100;
+ for (let x = Math.floor(bounds.minX / gridSize) * gridSize; x <= bounds.maxX; x += gridSize) {
+ ctx.beginPath();
+ ctx.moveTo(x, bounds.minY);
+ ctx.lineTo(x, bounds.maxY);
+ ctx.stroke();
+ }
+ for (let y = Math.floor(bounds.minY / gridSize) * gridSize; y <= bounds.maxY; y += gridSize) {
+ ctx.beginPath();
+ ctx.moveTo(bounds.minX, y);
+ ctx.lineTo(bounds.maxX, y);
+ ctx.stroke();
+ }
+
+ // Draw nodes
+ nodes.forEach((n, index) => {
+ const x = n.pos?.[0] ?? n.x ?? 0;
+ const y = n.pos?.[1] ?? n.y ?? 0;
+ const w = n.size?.[0] ?? n.width ?? 100;
+ const h = n.size?.[1] ?? n.height ?? 60;
+
+ ctx.fillStyle = getNodeColor(n);
+ ctx.fillRect(x, y, w, h);
+
+ ctx.strokeStyle = toRGBA(colors.inputText, 0.8);
+ ctx.lineWidth = 1 / scale;
+ ctx.strokeRect(x, y, w, h);
+
+ if (scale > 0.3) {
+ ctx.fillStyle = colors.inputText;
+ ctx.font = `${Math.max(10, 12 / scale)}px Arial`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ const title = n.title || n.type || `Node ${index + 1}`;
+ ctx.fillText(title.substring(0, 15), x + w / 2, y + h / 2);
+ }
+ });
+
+ // Draw viewport indicator
+ const app = getApp();
+ if (app?.canvas?.ds) {
+ const ds = app.canvas.ds;
+ const mainCanvas = document.querySelector('canvas');
+
+ if (mainCanvas) {
+ const viewportX = -ds.offset[0];
+ const viewportY = -ds.offset[1];
+ const viewportW = mainCanvas.width / ds.scale;
+ const viewportH = mainCanvas.height / ds.scale;
+
+ ctx.fillStyle = toRGBA(colors.accent || '#4a90e2', 0.12);
+ ctx.fillRect(viewportX, viewportY, viewportW, viewportH);
+
+ ctx.strokeStyle = toRGBA(colors.accent || '#4a90e2', 0.8);
+ ctx.lineWidth = 2 / scale;
+ ctx.strokeRect(viewportX, viewportY, viewportW, viewportH);
+ }
+ }
+
+ ctx.restore();
+ panInfo.textContent = `Nodes: ${nodes.length} | Zoom: ${(zoom * 100).toFixed(0)}% | Pan: ${panX.toFixed(0)}, ${panY.toFixed(0)}`;
+ }
+
+ function navigateToPosition(canvasX, canvasY) {
+ const worldPos = canvasToWorld(canvasX, canvasY);
+ if (!worldPos) return;
+
+ const app = getApp();
+ const mainCanvas = document.querySelector('canvas');
+ if (!app?.canvas?.ds || !mainCanvas) return;
+
+ try {
+ // Center the main canvas on the clicked world position
+ app.canvas.ds.offset[0] = -worldPos.x + mainCanvas.width / 2;
+ app.canvas.ds.offset[1] = -worldPos.y + mainCanvas.height / 2;
+ app.canvas.setDirty(true, true);
+
+ // Update minimap to reflect the change
+ setTimeout(() => drawMinimap(), 50);
+ } catch (err) {
+ console.log("❌ Navigation error:", err);
+ }
+ }
+
+ function isClickInViewport(canvasX, canvasY) {
+ const app = getApp();
+ if (!app?.canvas?.ds) return false;
+
+ const ds = app.canvas.ds;
+ const mainCanvas = document.querySelector('canvas');
+ if (!mainCanvas) return false;
+
+ const worldPos = canvasToWorld(canvasX, canvasY);
+ if (!worldPos) return false;
+
+ // Check if click is inside viewport rectangle in world coordinates
+ const viewportX = -ds.offset[0];
+ const viewportY = -ds.offset[1];
+ const viewportW = mainCanvas.width / ds.scale;
+ const viewportH = mainCanvas.height / ds.scale;
+
+ return worldPos.x >= viewportX && worldPos.x <= viewportX + viewportW &&
+ worldPos.y >= viewportY && worldPos.y <= viewportY + viewportH;
+ }
+
+ function adjustPanToKeepNodesVisible() {
+ const nodes = getNodes();
+ if (!nodes?.length) return;
+
+ const canvas = currentDialog?.querySelector('canvas');
+ if (!canvas) return;
+
+ // Calculate bounds
+ const bounds = nodes.reduce((acc, n) => {
+ const x = n.pos?.[0] ?? n.x ?? 0;
+ const y = n.pos?.[1] ?? n.y ?? 0;
+ const w = n.size?.[0] ?? n.width ?? 100;
+ const h = n.size?.[1] ?? n.height ?? 60;
+
+ return {
+ minX: Math.min(acc.minX, x),
+ minY: Math.min(acc.minY, y),
+ maxX: Math.max(acc.maxX, x + w),
+ maxY: Math.max(acc.maxY, y + h)
+ };
+ }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
+
+ const width = bounds.maxX - bounds.minX;
+ const height = bounds.maxY - bounds.minY;
+ if (width <= 0 || height <= 0) return;
+
+ const baseScale = Math.min(canvas.width / Math.max(width, 1000), canvas.height / Math.max(height, 1000));
+ const scale = baseScale * zoom;
+
+ // If zoomed in too much, adjust pan to keep nodes centered
+ if (zoom > 2) {
+ const maxPanX = canvas.width / 4;
+ const maxPanY = canvas.height / 4;
+ panX = Math.max(-maxPanX, Math.min(maxPanX, panX));
+ panY = Math.max(-maxPanY, Math.min(maxPanY, panY));
+ }
+ }
+
+ function setupEventHandlers() {
+ const canvas = currentDialog.querySelector('canvas');
+ let isViewportDragging = false;
+
+ // Mouse handlers
+ canvas.addEventListener('mousedown', (e) => {
+ dragStartTime = Date.now();
+ dragStartX = e.clientX;
+ dragStartY = e.clientY;
+
+ const rect = canvas.getBoundingClientRect();
+ const canvasX = e.clientX - rect.left;
+ const canvasY = e.clientY - rect.top;
+
+ // Check if clicking inside viewport indicator
+ isViewportDragging = isClickInViewport(canvasX, canvasY);
+
+ if (isViewportDragging) {
+ canvas.style.cursor = 'grab';
+ }
+ });
+
+ canvas.addEventListener('mousemove', (e) => {
+ if (!dragStartTime) return;
+
+ const totalDelta = Math.abs(e.clientX - dragStartX) + Math.abs(e.clientY - dragStartY);
+
+ if (totalDelta > DRAG_THRESHOLD && !isDragging) {
+ isDragging = true;
+ if (isViewportDragging) {
+ canvas.style.cursor = 'grabbing';
+ } else {
+ canvas.style.cursor = 'move';
+ }
+ }
+
+ if (isDragging) {
+ if (isViewportDragging) {
+ // Move the viewport - convert movement to world coordinates
+ const app = getApp();
+ if (app?.canvas?.ds) {
+ const transform = getTransformState();
+ if (transform) {
+ // Scale movement by the inverse of the minimap scale
+ const movementScale = 1 / transform.scale;
+ app.canvas.ds.offset[0] -= e.movementX * movementScale;
+ app.canvas.ds.offset[1] -= e.movementY * movementScale;
+ app.canvas.setDirty(true, true);
+ }
+ }
+ } else {
+ // Pan the minimap view
+ panX += e.movementX;
+ panY += e.movementY;
+ }
+ drawMinimap();
+ }
+ });
+
+ canvas.addEventListener('mouseup', (e) => {
+ const clickDuration = Date.now() - dragStartTime;
+ const totalMovement = Math.abs(e.clientX - dragStartX) + Math.abs(e.clientY - dragStartY);
+
+ if (!isDragging && clickDuration < 500 && totalMovement < DRAG_THRESHOLD) {
+ if (!isViewportDragging) {
+ // Regular click-to-navigate (only if not clicking viewport)
+ const rect = canvas.getBoundingClientRect();
+ navigateToPosition(e.clientX - rect.left, e.clientY - rect.top);
+ }
+ }
+
+ isDragging = false;
+ isViewportDragging = false;
+ dragStartTime = 0;
+ canvas.style.cursor = 'crosshair';
+ });
+
+ canvas.addEventListener('wheel', (e) => {
+ e.preventDefault();
+ const oldZoom = zoom;
+ zoom = Math.max(0.1, Math.min(5, zoom * (e.deltaY > 0 ? 0.9 : 1.1)));
+
+ // Adjust pan to keep content centered when zooming
+ if (zoom !== oldZoom) {
+ adjustPanToKeepNodesVisible();
+ }
+
+ drawMinimap();
+ });
+
+ // Button handlers
+ currentDialog.querySelector('#close-btn').onclick = () => closeDialog();
+ currentDialog.querySelector('#legend-btn').onclick = () => {
+ const legend = currentDialog.querySelector('.legend');
+ const isVisible = legend.style.display !== 'none';
+ legend.style.display = isVisible ? 'none' : 'block';
+ if (!isVisible) updateLegend();
+ };
+ currentDialog.querySelector('#zoom-in').onclick = () => {
+ zoom = Math.min(zoom * 1.2, 5);
+ adjustPanToKeepNodesVisible();
+ drawMinimap();
+ };
+ currentDialog.querySelector('#zoom-out').onclick = () => {
+ zoom = Math.max(zoom / 1.2, 0.1);
+ adjustPanToKeepNodesVisible();
+ drawMinimap();
+ };
+ currentDialog.querySelector('#zoom-reset').onclick = () => {
+ zoom = 1;
+ panX = panY = 0;
+ drawMinimap();
+ };
+
+ // ESC key
+ const escHandler = (e) => e.key === 'Escape' && closeDialog();
+ document.addEventListener('keydown', escHandler);
+
+ return escHandler;
+ }
+
+ function createMinimapDialog() {
+ if (currentDialog) return;
+
+ const colors = getComfyUIColors();
+ const { containerWidth, containerHeight, canvasWidth, canvasHeight } = calculateDimensions();
+
+ // Clean up existing style
+ document.getElementById('minimap-style')?.remove();
+
+ // Create style tag
+ const style = document.createElement('style');
+ style.id = 'minimap-style';
+ style.textContent = createStyleCSS(colors);
+ document.head.appendChild(style);
+
+ // Create container
+ const container = document.createElement('div');
+ container.id = 'endless-minimap';
+ container.style.cssText = `
+ position: fixed;
+ top: 100px;
+ right: 20px;
+ width: ${containerWidth}px;
+ height: ${containerHeight}px;
+ background: ${colors.menu};
+ color: ${colors.inputText};
+ border: 1px solid ${colors.accent};
+ border-radius: 8px;
+ box-shadow: ${colors.shadow || '0 4px 12px rgba(0, 0, 0, 0.25)'};
+ z-index: 99999;
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ `;
+
+ container.innerHTML = `
+
+
Endless 🌊✨ Minimap
+
+ 🎨
+ ▫️
+ 🏠
+ ⬜
+ ❌
+
+
+
+
Nodes: 0 | Zoom: 100% | Pan: 0, 0
+ `;
+
+ document.body.appendChild(container);
+ currentDialog = container;
+
+ // Setup dragging
+ makeDraggable(container, container.querySelector('.drag-bar'));
+
+ // Setup event handlers
+ const escHandler = setupEventHandlers();
+
+ // Setup resize observer
+ resizeObserver = new ResizeObserver(() => {
+ const newAspectRatio = getCanvasAspectRatio();
+ const canvas = container.querySelector('canvas');
+ const currentAspectRatio = canvas.width / canvas.height;
+
+ if (Math.abs(newAspectRatio - currentAspectRatio) > 0.1) {
+ const { containerWidth: newContainerWidth, containerHeight: newContainerHeight, canvasWidth: newCanvasWidth, canvasHeight: newCanvasHeight } = calculateDimensions();
+
+ // Update container size
+ container.style.width = `${newContainerWidth}px`;
+ container.style.height = `${newContainerHeight}px`;
+
+ // Update canvas size
+ canvas.width = newCanvasWidth;
+ canvas.height = newCanvasHeight;
+
+ drawMinimap();
+ }
+ });
+
+ const mainCanvas = document.querySelector('canvas');
+ if (mainCanvas) resizeObserver.observe(mainCanvas);
+ resizeObserver.observe(document.body);
+
+ // Setup theme updates
+ unregisterThemeCallback = onThemeChange(updateTheme);
+
+ // Start animation loop
+ function updateLoop() {
+ drawMinimap();
+ animationId = setTimeout(updateLoop, 1000);
+ }
+ updateLoop();
+
+ // Setup cleanup
+ const originalRemove = container.remove.bind(container);
+ container.remove = function() {
+ clearTimeout(animationId);
+ document.removeEventListener('keydown', escHandler);
+ resizeObserver?.disconnect();
+ unregisterThemeCallback?.();
+ document.getElementById('minimap-style')?.remove();
+ currentDialog = null;
+ animationId = null;
+ unregisterThemeCallback = null;
+ resizeObserver = null;
+ originalRemove();
+ };
+ }
+
+ function closeDialog() {
+ currentDialog?.remove();
+ }
+
+ // Register tool
+ registerEndlessTool("Minimap", createMinimapDialog);
+})();
\ No newline at end of file
diff --git a/web/endless_node_loader.js b/web/endless_node_loader.js
new file mode 100644
index 0000000..24eba94
--- /dev/null
+++ b/web/endless_node_loader.js
@@ -0,0 +1,870 @@
+// ComfyUI Endless 🌊✨ Node Spawner - Optimized Version
+
+(function waitForHelpers() {
+ if (typeof window.EndlessHelpers === 'undefined') {
+ console.warn("⏳ Waiting for EndlessHelpers to be ready...");
+ setTimeout(waitForHelpers, 100);
+ return;
+ }
+
+ const {
+ registerEndlessTool,
+ onThemeChange,
+ getComfyUIColors,
+ toRGBA,
+ makeDraggable,
+ addButtonHoverEffects
+ } = window.EndlessHelpers;
+
+ console.log("✅ Endless Node Spawner loaded.");
+
+ // State management
+ let currentDialog = null;
+ let unregisterThemeCallback = null;
+ let allNodesData = [];
+ let currentFilter = '';
+ let searchTimeout = null;
+ let hoverTimeout = null;
+
+ // Persistent data
+ let recentlyUsedNodes = JSON.parse(localStorage.getItem('endlessNodeLoader_recentlyUsed') || '[]');
+ let searchHistory = JSON.parse(localStorage.getItem('endlessNodeLoader_searchHistory') || '[]');
+
+ // Constants
+ const MAX_RECENT = 15;
+ const MAX_HISTORY = 15;
+ const DEFAULT_SPACING = { x: 300, y: 150 };
+ const NODE_PADDING = 20;
+
+ function createStyleCSS(colors) {
+ return `
+ .dialog-container {
+ display: flex;
+ flex-direction: column;
+ height: 60vh;
+ width: 35vw;
+ min-width: 400px;
+ min-height: 300px;
+ background: ${colors.menu};
+ color: ${colors.inputText};
+ padding: 10px;
+ border: 1px solid ${colors.border};
+ border-radius: 8px;
+ box-shadow: ${colors.shadow || '0 4px 20px rgba(0,0,0,0.5)'};
+ z-index: 9999;
+ overflow: hidden;
+ box-sizing: border-box;
+ }
+ .dialog-title {
+ margin: 0 0 15px 0;
+ cursor: move;
+ user-select: none;
+ padding: 6px;
+ background: ${colors.menuSecondary};
+ color: ${colors.inputText};
+ border-radius: 4px;
+ font-size: 14px;
+ border-bottom: 1px solid ${colors.border};
+ }
+ .filter-section {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 8px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid ${colors.border};
+ }
+ .filter-row {
+ position: relative;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ }
+ .filter-input {
+ flex: 1;
+ background: ${colors.inputBg};
+ color: ${colors.inputText};
+ border: 1px solid ${colors.border};
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-size: 12px;
+ }
+ .filter-input:focus {
+ outline: none;
+ border-color: ${colors.accent};
+ }
+ .expand-btn {
+ background: ${colors.inputBg};
+ color: ${colors.inputText};
+ border: 1px solid ${colors.border};
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ }
+ .expand-btn:hover {
+ background: ${colors.hoverBg};
+ border-color: ${colors.accent};
+ }
+ .search-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 80px;
+ background: ${colors.menu};
+ border: 1px solid ${colors.border};
+ border-radius: 4px;
+ max-height: 150px;
+ overflow-y: auto;
+ z-index: 10000;
+ display: none;
+ }
+ .search-item {
+ padding: 6px 8px;
+ cursor: pointer;
+ font-size: 12px;
+ border-bottom: 1px solid ${colors.border};
+ }
+ .search-item:last-child { border-bottom: none; }
+ .search-item:hover { background: ${colors.hoverBg}; }
+ .counters {
+ display: flex;
+ justify-content: space-between;
+ font-size: 11px;
+ color: ${colors.descriptionText};
+ }
+ .counter-selected {
+ color: ${colors.accent};
+ font-weight: bold;
+ }
+ .recent-section {
+ flex: 0 0 auto;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ min-height: 30px;
+ max-height: 15%;
+ overflow-y: auto;
+ border-bottom: 1px solid ${colors.border};
+ padding-bottom: 6px;
+ margin-bottom: 6px;
+ }
+ .recent-chip {
+ background: ${toRGBA(colors.accent, 0.1)};
+ color: ${colors.inputText};
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all 0.2s ease;
+ }
+ .recent-chip:hover {
+ border-color: ${colors.accent};
+ background: ${toRGBA(colors.accent, 0.2)};
+ }
+ .node-list {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ border-bottom: 1px solid ${colors.border};
+ padding-bottom: 6px;
+ margin-bottom: 6px;
+ }
+ .category {
+ margin-bottom: 4px;
+ }
+ .category > summary {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 0;
+ list-style: none;
+ color: ${colors.inputText};
+ }
+ .category > summary::-webkit-details-marker { display: none; }
+ .category > summary::before {
+ content: "▶";
+ width: 12px;
+ text-align: center;
+ color: ${colors.descriptionText};
+ font-size: 10px;
+ transition: transform 0.2s ease;
+ }
+ .category[open] > summary::before {
+ transform: rotate(90deg);
+ color: ${colors.inputText};
+ }
+ .category > summary:hover {
+ background: ${colors.hoverBg};
+ border-radius: 4px;
+ }
+ .category ul {
+ margin: 4px 0;
+ padding-left: 1em;
+ }
+ .category li:hover {
+ background: ${colors.hoverBg};
+ border-radius: 4px;
+ }
+ .category input[type="checkbox"] {
+ accent-color: ${colors.accent};
+ }
+ .cat-btn {
+ background: ${colors.inputBg};
+ color: ${colors.inputText};
+ border: 1px solid ${colors.border};
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 3px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+ .cat-btn:hover {
+ background: ${colors.hoverBg};
+ border-color: ${colors.accent};
+ }
+ .cat-btn.select { background: ${toRGBA('#4CAF50', 0.1)}; }
+ .cat-btn.deselect { background: ${toRGBA('#f44336', 0.1)}; }
+ .footer {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ }
+ .btn-group {
+ display: flex;
+ gap: 8px;
+ }
+ .dialog-btn {
+ background: ${colors.inputBg};
+ color: ${colors.inputText};
+ border: 1px solid ${colors.border};
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+ .dialog-btn:hover {
+ background: ${colors.hoverBg};
+ border-color: ${colors.accent};
+ }
+ .dialog-btn.primary {
+ background: ${toRGBA(colors.accent || '#4CAF50', 0.2)};
+ border-color: ${colors.accent || '#4CAF50'};
+ }
+ .dialog-btn.secondary {
+ background: ${toRGBA(colors.errorText || '#f44336', 0.2)};
+ border-color: ${colors.errorText || '#f44336'};
+ }
+ .hidden { display: none !important; }
+ `;
+ }
+
+ function updateTheme() {
+ if (!currentDialog) return;
+
+ const colors = getComfyUIColors();
+ const styleTag = document.getElementById('node-loader-style');
+ if (styleTag) {
+ styleTag.textContent = createStyleCSS(colors);
+ }
+ }
+
+ function getApp() {
+ return window.app || window.comfyApp;
+ }
+
+ function getExistingNodePositions() {
+ const app = getApp();
+ const positions = [];
+
+ if (app?.graph?.nodes) {
+ app.graph.nodes.forEach(node => {
+ if (node.pos) {
+ positions.push({
+ x: node.pos[0],
+ y: node.pos[1],
+ width: node.size?.[0] || 200,
+ height: node.size?.[1] || 100
+ });
+ }
+ });
+ }
+ return positions;
+ }
+
+ function findNonOverlappingPosition(startX, startY, width, height, existingPositions, spacingX, spacingY) {
+ let x = startX;
+ let y = startY;
+
+ while (true) {
+ let overlaps = false;
+
+ for (const pos of existingPositions) {
+ if (!(x + width + NODE_PADDING < pos.x ||
+ x - NODE_PADDING > pos.x + pos.width ||
+ y + height + NODE_PADDING < pos.y ||
+ y - NODE_PADDING > pos.y + pos.height)) {
+ overlaps = true;
+ break;
+ }
+ }
+
+ if (!overlaps) return { x, y };
+
+ x += spacingX;
+ if (x > startX + spacingX * 5) {
+ x = startX;
+ y += spacingY;
+ }
+ }
+ }
+
+ function spawnNodes(types, spacingX = DEFAULT_SPACING.x, spacingY = DEFAULT_SPACING.y) {
+ const app = getApp();
+ if (!app?.graph?.add) {
+ alert("ComfyUI graph not available.");
+ return;
+ }
+
+ const startX = -app.canvas.ds.offset[0] + 50;
+ const startY = -app.canvas.ds.offset[1] + 50;
+ const existingPositions = getExistingNodePositions();
+
+ const spawnedNodes = [];
+
+ types.forEach((type, i) => {
+ const node = LiteGraph.createNode(type);
+ if (node) {
+ const nodeWidth = node.size?.[0] || 200;
+ const nodeHeight = node.size?.[1] || 100;
+
+ const position = findNonOverlappingPosition(
+ startX + (i % 5) * spacingX,
+ startY + Math.floor(i / 5) * spacingY,
+ nodeWidth,
+ nodeHeight,
+ existingPositions,
+ spacingX,
+ spacingY
+ );
+
+ node.pos = [position.x, position.y];
+ app.graph.add(node);
+ spawnedNodes.push(type);
+
+ existingPositions.push({
+ x: position.x,
+ y: position.y,
+ width: nodeWidth,
+ height: nodeHeight
+ });
+ } else {
+ console.warn(`Could not create node: ${type}`);
+ }
+ });
+
+ updateRecentlyUsedNodes(spawnedNodes);
+ app.graph.setDirtyCanvas(true, true);
+ }
+
+ function updateRecentlyUsedNodes(newNodes) {
+ newNodes.forEach(nodeType => {
+ const index = recentlyUsedNodes.indexOf(nodeType);
+ if (index > -1) recentlyUsedNodes.splice(index, 1);
+ recentlyUsedNodes.unshift(nodeType);
+ });
+
+ recentlyUsedNodes = recentlyUsedNodes.slice(0, MAX_RECENT);
+ localStorage.setItem('endlessNodeLoader_recentlyUsed', JSON.stringify(recentlyUsedNodes));
+
+ if (currentDialog) updateRecentChips();
+ }
+
+ function updateRecentChips() {
+ const recentSection = currentDialog.querySelector('.recent-section');
+ if (!recentSection) return;
+
+ recentSection.innerHTML = '';
+
+ recentlyUsedNodes.forEach(nodeType => {
+ const chip = document.createElement('button');
+ chip.className = 'recent-chip';
+
+ const nodeClass = LiteGraph.registered_node_types[nodeType];
+ const displayName = nodeClass?.title || nodeClass?.name || nodeType.split("/").pop();
+ chip.textContent = displayName;
+ chip.title = nodeType;
+
+ chip.onclick = () => {
+ const checkbox = Array.from(currentDialog.querySelectorAll('.node-checkbox')).find(cb => {
+ return cb.closest('li').dataset.nodeType === nodeType;
+ });
+ if (checkbox) {
+ checkbox.checked = true;
+ updateSelectedCounter();
+ }
+ };
+
+ recentSection.appendChild(chip);
+ });
+ }
+
+ function addToSearchHistory(searchTerm) {
+ if (!searchTerm.trim() || searchHistory.includes(searchTerm)) return;
+
+ searchHistory.unshift(searchTerm);
+ searchHistory = searchHistory.slice(0, MAX_HISTORY);
+ localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory));
+ }
+
+ function showSearchHistory(inputElement) {
+ const dropdown = inputElement.parentElement.querySelector('.search-dropdown');
+ if (!dropdown || searchHistory.length === 0) {
+ if (dropdown) dropdown.style.display = 'none';
+ return;
+ }
+
+ dropdown.innerHTML = '';
+ searchHistory.forEach(term => {
+ const item = document.createElement('div');
+ item.className = 'search-item';
+ item.textContent = term;
+ item.onclick = () => {
+ inputElement.value = term;
+ applyFilter(term, true);
+ hideSearchHistory(dropdown);
+ };
+ dropdown.appendChild(item);
+ });
+
+ dropdown.style.display = 'block';
+
+ setTimeout(() => {
+ if (dropdown.style.display === 'block') {
+ hideSearchHistory(dropdown);
+ }
+ }, 10000);
+ }
+
+ function hideSearchHistory(dropdown) {
+ dropdown.style.display = 'none';
+ }
+
+ function applyFilter(filterText, saveToHistory = true) {
+ currentFilter = filterText.toLowerCase();
+ const nodeList = currentDialog.querySelector('.node-list');
+
+ if (!currentFilter) {
+ nodeList.querySelectorAll('.category, .category li').forEach(el => {
+ el.classList.remove('hidden');
+ });
+ updateTotalCounter();
+ return;
+ }
+
+ nodeList.querySelectorAll('.category').forEach(details => {
+ const categoryName = details.querySelector('summary span').textContent.toLowerCase();
+ const categoryMatches = categoryName.includes(currentFilter);
+
+ let hasMatchingNodes = false;
+ const nodeItems = details.querySelectorAll('li');
+
+ nodeItems.forEach(li => {
+ const nodeText = li.textContent.toLowerCase();
+ const nodeType = li.dataset.nodeType?.toLowerCase() || '';
+ const matches = nodeText.includes(currentFilter) || nodeType.includes(currentFilter);
+
+ if (matches) {
+ li.classList.remove('hidden');
+ hasMatchingNodes = true;
+ } else {
+ li.classList.add('hidden');
+ }
+ });
+
+ if (categoryMatches || hasMatchingNodes) {
+ details.classList.remove('hidden');
+ if (hasMatchingNodes && !categoryMatches) {
+ details.open = true;
+ }
+ } else {
+ details.classList.add('hidden');
+ }
+ });
+
+ updateTotalCounter();
+ }
+
+ function updateSelectedCounter() {
+ const counter = currentDialog.querySelector('.counter-selected');
+ if (!counter) return;
+
+ const checkedBoxes = currentDialog.querySelectorAll('.node-checkbox:checked');
+ counter.textContent = `Selected: ${checkedBoxes.length}`;
+ }
+
+ function updateTotalCounter() {
+ const counter = currentDialog.querySelector('.counter-total');
+ if (!counter) return;
+
+ const visibleNodes = currentDialog.querySelectorAll('.category li:not(.hidden)');
+ counter.textContent = `Total: ${visibleNodes.length}/${allNodesData.length}`;
+ }
+
+ function toggleAllCategories(expand) {
+ const details = currentDialog.querySelectorAll('.category:not(.hidden)');
+ details.forEach(detail => {
+ detail.open = expand;
+ });
+ }
+
+ function buildHierarchy(nodes) {
+ const root = {};
+ nodes.forEach(n => {
+ let current = root;
+ n.pathParts.forEach((part, idx) => {
+ if (!current[part]) {
+ current[part] = { _nodes: [], _subcategories: {} };
+ }
+ if (idx === n.pathParts.length - 1) {
+ current[part]._nodes.push(n);
+ } else {
+ current = current[part]._subcategories;
+ }
+ });
+ });
+ return root;
+ }
+
+ function countNodesInCategory(categoryObj) {
+ let count = categoryObj._nodes?.length || 0;
+ if (categoryObj._subcategories) {
+ Object.values(categoryObj._subcategories).forEach(sub => {
+ count += countNodesInCategory(sub);
+ });
+ }
+ return count;
+ }
+
+ function selectAllInCategory(categoryDetails, select = true) {
+ const checkboxes = categoryDetails.querySelectorAll("input[type='checkbox']");
+ checkboxes.forEach(checkbox => {
+ checkbox.checked = select;
+ });
+ updateSelectedCounter();
+ }
+
+ function renderCategory(categoryObj, depth = 0) {
+ return Object.entries(categoryObj)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([cat, obj]) => {
+ const totalNodes = countNodesInCategory(obj);
+
+ const details = document.createElement("details");
+ details.className = "category";
+ details.style.paddingLeft = `${depth * 1.2}em`;
+
+ const summary = document.createElement("summary");
+
+ const categoryName = document.createElement("span");
+ categoryName.textContent = `${cat} (${totalNodes})`;
+
+ const selectAllBtn = document.createElement("button");
+ selectAllBtn.textContent = "All";
+ selectAllBtn.className = "cat-btn select";
+ selectAllBtn.onclick = (e) => {
+ e.stopPropagation();
+ selectAllInCategory(details, true);
+ };
+
+ const selectNoneBtn = document.createElement("button");
+ selectNoneBtn.textContent = "None";
+ selectNoneBtn.className = "cat-btn deselect";
+ selectNoneBtn.onclick = (e) => {
+ e.stopPropagation();
+ selectAllInCategory(details, false);
+ };
+
+ summary.appendChild(categoryName);
+ summary.appendChild(selectAllBtn);
+ summary.appendChild(selectNoneBtn);
+ details.appendChild(summary);
+
+ const list = document.createElement("ul");
+ (obj._nodes || []).forEach(node => {
+ const li = document.createElement("li");
+ li.dataset.nodeType = node.type;
+
+ const checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.className = "node-checkbox";
+ checkbox.onchange = updateSelectedCounter;
+
+ const label = document.createElement("label");
+ label.textContent = node.displayName;
+
+ li.appendChild(checkbox);
+ li.appendChild(label);
+ list.appendChild(li);
+ });
+
+ details.appendChild(list);
+
+ const subCategories = renderCategory(obj._subcategories || {}, depth + 1);
+ subCategories.forEach(sub => details.appendChild(sub));
+
+ return details;
+ });
+ }
+
+ function getSelectedNodeTypes() {
+ const selected = [];
+ currentDialog.querySelectorAll('.node-checkbox:checked').forEach(checkbox => {
+ const nodeType = checkbox.closest('li').dataset.nodeType;
+ if (nodeType) selected.push(nodeType);
+ });
+ return selected;
+ }
+
+ function clearSelectedNodes() {
+ currentDialog.querySelectorAll('.node-checkbox:checked').forEach(checkbox => {
+ checkbox.checked = false;
+ });
+ updateSelectedCounter();
+ }
+
+ function setupEventHandlers() {
+ const filterInput = currentDialog.querySelector('.filter-input');
+ const searchDropdown = currentDialog.querySelector('.search-dropdown');
+ const expandBtn = currentDialog.querySelector('.expand-btn');
+
+ let isExpanded = false;
+
+ // Filter input handlers
+ filterInput.oninput = (e) => applyFilter(e.target.value, false);
+
+ filterInput.onkeydown = (e) => {
+ if (e.key === 'Enter' && e.target.value.trim()) {
+ addToSearchHistory(e.target.value.trim());
+ hideSearchHistory(searchDropdown);
+ } else if (e.key === 'ArrowDown' && searchHistory.length > 0) {
+ showSearchHistory(filterInput);
+ e.preventDefault();
+ }
+ };
+
+ filterInput.onfocus = (e) => {
+ if (!e.target.value.trim()) {
+ showSearchHistory(filterInput);
+ }
+ };
+
+ filterInput.onblur = (e) => {
+ if (e.target.value.trim()) {
+ addToSearchHistory(e.target.value.trim());
+ }
+ if (hoverTimeout) {
+ clearTimeout(hoverTimeout);
+ hoverTimeout = null;
+ }
+ setTimeout(() => hideSearchHistory(searchDropdown), 150);
+ };
+
+ filterInput.onmouseenter = () => {
+ if (searchTimeout) {
+ clearTimeout(searchTimeout);
+ searchTimeout = null;
+ }
+
+ hoverTimeout = setTimeout(() => {
+ if (searchHistory.length > 0) {
+ showSearchHistory(filterInput);
+ }
+ }, 1000);
+ };
+
+ filterInput.onmouseleave = () => {
+ if (hoverTimeout) {
+ clearTimeout(hoverTimeout);
+ hoverTimeout = null;
+ }
+
+ searchTimeout = setTimeout(() => {
+ hideSearchHistory(searchDropdown);
+ }, 10000);
+ };
+
+ // Expand button
+ expandBtn.onclick = () => {
+ isExpanded = !isExpanded;
+ toggleAllCategories(isExpanded);
+ expandBtn.textContent = isExpanded ? "Collapse All" : "Expand All";
+ };
+
+ // Button handlers
+ currentDialog.querySelector('#spawn-btn').onclick = () => {
+ const selectedTypes = getSelectedNodeTypes();
+ if (selectedTypes.length === 0) {
+ alert("Please select at least one node to spawn.");
+ return;
+ }
+ spawnNodes(selectedTypes);
+ closeDialog();
+ };
+
+ currentDialog.querySelector('#clear-btn').onclick = clearSelectedNodes;
+ currentDialog.querySelector('#cancel-btn').onclick = closeDialog;
+ currentDialog.querySelector('#clear-history-btn').onclick = () => {
+ searchHistory = [];
+ localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory));
+ const dropdown = currentDialog.querySelector('.search-dropdown');
+ if (dropdown) dropdown.style.display = 'none';
+ };
+ currentDialog.querySelector('#clear-recent-btn').onclick = () => {
+ recentlyUsedNodes = [];
+ localStorage.setItem('endlessNodeLoader_recentlyUsed', JSON.stringify(recentlyUsedNodes));
+ updateRecentChips(); // This will clear the chips
+ };
+
+ // ESC key handler
+ const escHandler = (e) => e.key === 'Escape' && closeDialog();
+ document.addEventListener('keydown', escHandler);
+
+ return escHandler;
+ }
+
+ function createNodeLoaderDialog() {
+ if (currentDialog) return;
+
+ const colors = getComfyUIColors();
+
+ // Clean up existing style
+ document.getElementById('node-loader-style')?.remove();
+
+ // Create style tag
+ const style = document.createElement('style');
+ style.id = 'node-loader-style';
+ style.textContent = createStyleCSS(colors);
+ document.head.appendChild(style);
+
+ // Create container
+ const container = document.createElement("div");
+ // Calculate max size based on viewport
+ const maxWidth = Math.min(window.innerWidth * 0.4, 1536); // 40% of window width, max 1536px
+ const maxHeight = Math.min(window.innerHeight * 0.6, 1296); // 60% of window height, max 1296px
+
+ container.className = "dialog-container";
+ container.style.cssText = `
+ position: fixed;
+ top: 10%;
+ left: 50%;
+ transform: translateX(-50%);
+ max-width: ${maxWidth}px;
+ max-height: ${maxHeight}px;
+ `;
+
+ container.innerHTML = `
+
Endless 🌊✨ Node Spawner Drag Bar
+
+
+
+
+ Selected: 0
+ Total: 0
+
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(container);
+ currentDialog = container;
+
+ // Setup dragging
+ makeDraggable(container, container.querySelector('.dialog-title'));
+
+ // Build node data
+ const nodes = Object.entries(LiteGraph.registered_node_types)
+ .filter(([key, value]) => key && value)
+ .map(([type, nodeClass]) => {
+ const category = nodeClass.category || "Other";
+ const displayName = nodeClass.title || nodeClass.name || type.split("/").pop();
+ return {
+ type,
+ category,
+ pathParts: category.split("/"),
+ displayName,
+ description: nodeClass.desc || nodeClass.description || "",
+ fullPath: category + "/" + displayName
+ };
+ })
+ .sort((a, b) => a.category.localeCompare(b.category) || a.displayName.localeCompare(b.displayName));
+
+ allNodesData = nodes;
+
+ // Render hierarchy
+ const hierarchy = buildHierarchy(nodes);
+ const tree = renderCategory(hierarchy);
+ const nodeList = container.querySelector('.node-list');
+ tree.forEach(section => nodeList.appendChild(section));
+
+ // Add hover effects
+ addButtonHoverEffects(container);
+
+ // Setup event handlers
+ const escHandler = setupEventHandlers();
+
+ // Setup theme updates
+ unregisterThemeCallback = onThemeChange(updateTheme);
+
+ // Initialize
+ updateRecentChips();
+ updateSelectedCounter();
+ updateTotalCounter();
+
+ // Focus filter input
+ container.querySelector('.filter-input').focus();
+
+ // Setup cleanup
+ const originalRemove = container.remove.bind(container);
+ container.remove = function() {
+ document.removeEventListener('keydown', escHandler);
+ unregisterThemeCallback?.();
+ document.getElementById('node-loader-style')?.remove();
+ if (searchTimeout) clearTimeout(searchTimeout);
+ if (hoverTimeout) clearTimeout(hoverTimeout);
+ currentDialog = null;
+ unregisterThemeCallback = null;
+ searchTimeout = null;
+ hoverTimeout = null;
+ originalRemove();
+ };
+ }
+
+ function closeDialog() {
+ currentDialog?.remove();
+ }
+
+ // Register tool
+ registerEndlessTool("Node Spawner", createNodeLoaderDialog);
+})();
\ No newline at end of file
diff --git a/web/endless_ui_helpers.js b/web/endless_ui_helpers.js
new file mode 100644
index 0000000..e7a78ba
--- /dev/null
+++ b/web/endless_ui_helpers.js
@@ -0,0 +1,276 @@
+// === Endless 🌊✨ Tools UI Helper ===
+
+const endlessToolsRegistry = [];
+
+export function registerEndlessTool(name, callback) {
+ endlessToolsRegistry.push({ name, callback });
+}
+
+export function injectEndlessToolsButton() {
+ const toolbar = findToolbar();
+ if (!toolbar || document.getElementById("endless-tools-button")) return;
+
+ const btn = document.createElement("button");
+ btn.id = "endless-tools-button";
+ btn.textContent = "Endless 🌊✨ Tools";
+ btn.className = "comfyui-button";
+ btn.style.marginLeft = "8px";
+ btn.onclick = showEndlessToolMenu;
+ toolbar.appendChild(btn);
+}
+
+export function showEndlessToolMenu() {
+ document.getElementById("endless-tools-float")?.remove();
+
+ const colors = getComfyUIColors();
+
+ const menu = document.createElement("div");
+ menu.id = "endless-tools-float";
+ menu.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: ${colors.menu};
+ color: ${colors.inputText};
+ padding: 12px;
+ border: 1px solid ${colors.accent};
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
+ z-index: 99999;
+ opacity: 1;
+ width: fit-content;
+ transition: opacity 0.2s ease;
+ `;
+
+ const dragBar = document.createElement("div");
+ dragBar.textContent = "Endless 🌊✨ Tools Drag Bar";
+ dragBar.style.cssText = `
+ padding: 4px;
+ background: ${toRGBA(colors.inputText, 0.05)};
+ cursor: move;
+ font-size: 12px;
+ text-align: center;
+ user-select: none;
+ border-bottom: 1px solid ${colors.border};
+ `;
+ menu.appendChild(dragBar);
+
+ endlessToolsRegistry.sort((a, b) => a.name.localeCompare(b.name)).forEach(tool => {
+ const btn = document.createElement("div");
+ btn.textContent = `🌊✨ ${tool.name}`;
+ btn.style.cssText = `
+ padding: 6px 10px;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: background 0.2s ease;
+ `;
+ btn.onmouseover = () => btn.style.background = toRGBA(colors.inputText, 0.1);
+ btn.onmouseout = () => btn.style.background = "transparent";
+ btn.onclick = () => {
+ tool.callback();
+ menu.remove();
+ };
+ menu.appendChild(btn);
+ });
+
+ makeDraggable(menu, dragBar);
+
+ // Live theme updater
+ function updateMenuTheme(newColors = getComfyUIColors()) {
+ menu.style.background = newColors.menu;
+ menu.style.color = newColors.inputText;
+ menu.style.borderColor = newColors.accent;
+ menu.style.boxShadow = newColors.shadow;
+ dragBar.style.background = toRGBA(newColors.inputText, 0.05);
+ dragBar.style.borderBottomColor = newColors.border;
+ }
+
+ const unregister = onThemeChange(updateMenuTheme);
+ menu.remove = ((orig => function () {
+ unregister();
+ orig.call(this);
+ })(menu.remove));
+
+ document.body.appendChild(menu);
+}
+
+// === Hotkeys ===
+document.addEventListener('keydown', (e) => {
+ if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'e') {
+ showEndlessToolMenu();
+ e.preventDefault();
+ }
+ if (e.key === "Escape") {
+ document.getElementById("endless-tools-float")?.remove();
+ }
+});
+
+console.log("Endless 🌊✨ Tools menu: press Ctrl+Alt+E if toolbar button is missing.");
+
+function waitForToolbarAndInject() {
+ if (document.querySelector('.comfyui-menu')) {
+ injectEndlessToolsButton();
+ return;
+ }
+ const observer = new MutationObserver(() => {
+ if (document.querySelector('.comfyui-menu')) {
+ injectEndlessToolsButton();
+ observer.disconnect();
+ }
+ });
+ observer.observe(document.body, { childList: true, subtree: true });
+}
+waitForToolbarAndInject();
+
+function findToolbar() {
+ return (
+ document.querySelector('.comfyui-menu, .comfy-menu, [class*="menu"], [class*="toolbar"]') ||
+ Array.from(document.querySelectorAll('[class*="button-group"], [class*="btn-group"], .comfyui-button-group'))
+ .find(g => g.querySelectorAll('button').length > 0) ||
+ Array.from(document.querySelectorAll('*'))
+ .find(el => {
+ const buttons = el.querySelectorAll('button');
+ return buttons.length >= 2 && buttons.length <= 10;
+ }) ||
+ Array.from(document.querySelectorAll(".comfyui-button-group"))
+ .find(div => Array.from(div.querySelectorAll("button")).some(btn => btn.title === "Share"))
+ );
+}
+
+// === Live Theme Monitoring ===
+let themeObserver = null;
+const themeCallbacks = new Set();
+
+export function onThemeChange(callback) {
+ themeCallbacks.add(callback);
+ if (themeCallbacks.size === 1) startThemeObserver();
+ return () => {
+ themeCallbacks.delete(callback);
+ if (themeCallbacks.size === 0) stopThemeObserver();
+ };
+}
+
+function startThemeObserver() {
+ if (themeObserver) return;
+ themeObserver = new MutationObserver(() => {
+ clearTimeout(window.themeChangeTimeout);
+ window.themeChangeTimeout = setTimeout(() => {
+ const newColors = getComfyUIColors();
+ themeCallbacks.forEach(cb => cb(newColors));
+ }, 100);
+ });
+ themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
+ themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
+}
+
+function stopThemeObserver() {
+ if (themeObserver) {
+ themeObserver.disconnect();
+ themeObserver = null;
+ }
+}
+
+export function getComfyUIColors() {
+ const computed = getComputedStyle(document.documentElement);
+ const getVar = name => computed.getPropertyValue(name).trim() || null;
+ return {
+ fg: getVar("--fg-color") || "#ddd",
+ bg: getVar("--bg-color") || "#353535",
+ menu: getVar("--comfy-menu-bg") || "#353535",
+ menuSecondary: getVar("--comfy-menu-secondary-bg") || "#222",
+ inputBg: getVar("--comfy-input-bg") || "#222",
+ inputText: getVar("--input-text") || "#ddd",
+ descriptionText: getVar("--descrip-text") || "#999",
+ dragText: getVar("--drag-text") || "#ddd",
+ errorText: getVar("--error-text") || "#f44336",
+ border: getVar("--border-color") || "#999",
+ accent: getVar("--comfy-accent") || getVar("--comfy-accent-color") || "#4a90e2",
+ hoverBg: getVar("--content-hover-bg") || "rgba(255,255,255,0.1)",
+ hoverFg: getVar("--content-hover-fg") || "#fff",
+ shadow: getVar("--bar-shadow") || "0 2px 10px rgba(0,0,0,0.3)",
+ dialogBg: getVar("--comfy-menu-bg") || getVar("--bg-color") || "#353535",
+ buttonHoverBg: getVar("--content-hover-bg") || "rgba(255,255,255,0.1)"
+ };
+}
+
+export function toRGBA(color, alpha = 0.2) {
+ if (!color) return `rgba(128,128,128,${alpha})`;
+ color = color.trim();
+ if (color.startsWith('#')) {
+ const hex = color.slice(1);
+ const fullHex = hex.length === 3 ? hex.split('').map(c => c + c).join('') : hex;
+ const bigint = parseInt(fullHex, 16);
+ const r = (bigint >> 16) & 255, g = (bigint >> 8) & 255, b = bigint & 255;
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+ }
+ if (color.startsWith('rgb')) {
+ const rgb = color.match(/\d+/g);
+ if (rgb?.length >= 3) return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
+ }
+ return `rgba(128,128,128,${alpha})`;
+}
+
+export function blendColors(color1, color2, ratio) {
+ const c1 = toRGBA(color1, 1).match(/\d+/g);
+ const c2 = toRGBA(color2, 1).match(/\d+/g);
+ if (!c1 || !c2) return color1;
+ const r = Math.round(c1[0] * (1 - ratio) + c2[0] * ratio);
+ const g = Math.round(c1[1] * (1 - ratio) + c2[1] * ratio);
+ const b = Math.round(c1[2] * (1 - ratio) + c2[2] * ratio);
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+export function addButtonHoverEffects(container) {
+ container?.querySelectorAll('button').forEach(button => {
+ button.addEventListener('mouseenter', () => {
+ button.style.boxShadow = '0 0 0 1px currentColor';
+ button.style.filter = 'brightness(1.1)';
+ button.style.transform = 'translateY(-1px)';
+ });
+ button.addEventListener('mouseleave', () => {
+ button.style.boxShadow = 'none';
+ button.style.filter = 'brightness(1)';
+ button.style.transform = 'translateY(0px)';
+ });
+ });
+}
+
+export function makeDraggable(element, handle = element) {
+ let offsetX = 0, offsetY = 0, isDown = false;
+ handle.onmousedown = (e) => {
+ isDown = true;
+ if (element.style.position !== 'fixed') {
+ element.style.position = 'fixed';
+ element.style.right = 'auto';
+ }
+ const rect = element.getBoundingClientRect();
+ offsetX = e.clientX - rect.left;
+ offsetY = e.clientY - rect.top;
+ element.style.cursor = 'move';
+ document.onmousemove = (e) => {
+ if (!isDown) return;
+ element.style.left = `${e.clientX - offsetX}px`;
+ element.style.top = `${e.clientY - offsetY}px`;
+ element.style.transform = 'none';
+ };
+ document.onmouseup = () => {
+ isDown = false;
+ element.style.cursor = 'default';
+ document.onmousemove = null;
+ document.onmouseup = null;
+ };
+ };
+}
+
+// === Global exposure for F12 ===
+window.EndlessHelpers = {
+ registerEndlessTool,
+ injectEndlessToolsButton,
+ showEndlessToolMenu,
+ onThemeChange,
+ getComfyUIColors,
+ toRGBA,
+ blendColors,
+ addButtonHoverEffects,
+ makeDraggable
+};