From 759ba45c15f76ab35ddf87a296d0a6337fc3004f Mon Sep 17 00:00:00 2001
From: AI Lab <129358391+1038lab@users.noreply.github.com>
Date: Thu, 18 Dec 2025 21:57:31 -0800
Subject: [PATCH] Add files via upload
---
web/NodeAlign.js | 435 +++++++++++++++++++++++++++++++++++++++++++++++
web/settings.js | 158 +++++++++++++++++
2 files changed, 593 insertions(+)
create mode 100644 web/NodeAlign.js
create mode 100644 web/settings.js
diff --git a/web/NodeAlign.js b/web/NodeAlign.js
new file mode 100644
index 0000000..af7fc51
--- /dev/null
+++ b/web/NodeAlign.js
@@ -0,0 +1,435 @@
+import { app } from "../../scripts/app.js";
+
+const MENU_SELECTORS = [".comfyui-menu", ".comfy-menu", '[data-testid="comfy-menu"]'];
+const CANVAS_SELECTORS = ["#graph-canvas", "canvas#graph-canvas", "canvas.litegraph", '[data-testid="graph-canvas"]'];
+
+const icons = {
+ bottom: ``,
+ centerX: ``,
+ centerY: ``,
+ left: ``,
+ right: ``,
+ top: ``,
+ equalW: ``,
+ equalH: ``,
+ distX: ``,
+ distY: ``
+};
+
+const NodeAlignFull = {
+ initialized: false,
+ toolbar: null,
+ menu: null,
+ canvasEl: null,
+ insertionIndicator: null,
+ dragState: { active: false, offsetX: 0, offsetY: 0 },
+ position: { leftPct: 50, topPct: 5, attached: false, insertIndex: 0 },
+ rememberPosition: true,
+ displayRaf: null,
+
+ async init() {
+ if (this.initialized) return;
+ await this.waitForDom();
+ this.logCompat();
+ this.loadPosition();
+ this.injectStyles();
+ this.buildToolbar();
+ this.applyLayoutSettings();
+ this.restorePosition();
+ this.bindCanvasEvents();
+ this.bindShortcuts();
+ this.initialized = true;
+ this.updateDisplay();
+ },
+
+ waitForDom() {
+ return new Promise(resolve => {
+ const tick = () => {
+ this.menu = MENU_SELECTORS.map(sel => document.querySelector(sel)).find(Boolean);
+ this.canvasEl = app.canvas?.canvas || CANVAS_SELECTORS.map(sel => document.querySelector(sel)).find(Boolean);
+ if (this.menu && this.canvasEl) return resolve();
+ requestAnimationFrame(tick);
+ };
+ tick();
+ });
+ },
+
+ getSetting(key, fallback) {
+ const v = app.ui?.settings?.getSettingValue?.(key);
+ return v ?? fallback;
+ },
+
+ loadPosition() {
+ this.rememberPosition = this.getSetting("NodeAlign.RememberPosition", true);
+ const saved = this.rememberPosition ? JSON.parse(localStorage.getItem("NodeAlignFullPosition") || "{}") : {};
+ this.position.leftPct = saved.leftPct ?? 50;
+ this.position.topPct = saved.topPct ?? 5;
+ this.position.attached = saved.attached ?? false;
+ this.position.insertIndex = saved.insertIndex ?? 0;
+ if (!this.rememberPosition) localStorage.removeItem("NodeAlignFullPosition");
+ },
+
+ savePosition() {
+ if (!this.rememberPosition) return;
+ localStorage.setItem("NodeAlignFullPosition", JSON.stringify(this.position));
+ },
+
+ injectStyles() {
+ if (document.getElementById("nodealign-full-style")) return;
+ const dividerColor = this.getSetting("NodeAlign.NodeAlignDividerColor", "666");
+ const style = document.createElement("style");
+ style.id = "nodealign-full-style";
+ style.textContent = `
+ #nodealign-full-toolbar{display:flex;align-items:center;gap:4px;padding:4px;border-radius:4px;z-index:10000;white-space:nowrap;user-select:none;pointer-events:auto;flex-wrap:nowrap;}
+ #nodealign-full-toolbar.floating{position:fixed;}
+ #nodealign-full-toolbar.attached{position:relative;margin-left:10px;}
+ .nodealign-btn{width:var(--nodealign-btn-size,25px);height:var(--nodealign-btn-size,25px);display:flex;align-items:center;justify-content:center;background-color:var(--nodealign-icon-bg);border:none;cursor:pointer;padding:0;border-radius:4px;transition:background-color .2s ease,transform .1s ease;flex-shrink:0;position:relative;overflow:hidden;}
+ .nodealign-btn svg{width:66%;height:66%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}
+ .nodealign-divider{width:var(--nodealign-divider-w,3px);height:var(--nodealign-divider-h,15px);background:#${dividerColor};border-radius:9px;cursor:grab;flex-shrink:0;}
+ .nodealign-divider:active{cursor:grabbing;}
+ .comfy-menu,.comfyui-menu{position:relative;display:flex;align-items:center;}
+ `;
+ document.head.appendChild(style);
+ },
+
+ buildToolbar() {
+ if (document.getElementById("nodealign-full-toolbar")) return;
+ const bg = this.getSetting("NodeAlign.NodeAlignBackgroundColor", "1b1b1b");
+ const opacity = this.getSetting("NodeAlign.NodeAlignBackgroundOpacity", 70) / 100;
+ const iconBg = this.getSetting("NodeAlign.NodeAlignIconBackgroundColor", "2a2a2a");
+ const iconColor = this.getSetting("NodeAlign.NodeAlignIconColor", "e0e0e0");
+
+ this.toolbar = document.createElement("div");
+ this.toolbar.id = "nodealign-full-toolbar";
+ this.toolbar.classList.add(this.position.attached ? "attached" : "floating");
+ this.toolbar.style.setProperty("--nodealign-icon-bg", `#${iconBg}`);
+ this.toolbar.style.background = `rgba(${parseInt(bg.slice(0,2),16)}, ${parseInt(bg.slice(2,4),16)}, ${parseInt(bg.slice(4,6),16)}, ${opacity})`;
+
+ const addBtn = (id, icon, action, title) => {
+ const el = document.createElement("button");
+ el.className = "nodealign-btn";
+ el.id = id;
+ el.title = title || "";
+ el.innerHTML = icon.replace(/#666/g, `#${iconColor}`);
+ el.addEventListener("click", action);
+ el.addEventListener("mouseover", () => el.style.backgroundColor = this.adjustColor(iconBg, 60));
+ el.addEventListener("mouseout", () => el.style.backgroundColor = `#${iconBg}`);
+ el.addEventListener("mousedown", () => el.style.transform = "scale(0.95)");
+ el.addEventListener("mouseup", () => el.style.transform = "");
+ el.addEventListener("mouseleave", () => el.style.transform = "");
+ this.toolbar.appendChild(el);
+ };
+
+ const addDivider = () => {
+ const div = document.createElement("div");
+ div.className = "nodealign-divider";
+ div.addEventListener("mousedown", e => this.onDragStart(e));
+ div.addEventListener("dblclick", () => this.resetPosition(true));
+ this.toolbar.appendChild(div);
+ };
+
+ addBtn("align-left", icons.left, () => this.align("left"), "Align Left");
+ addBtn("align-centerY", icons.centerY, () => this.align("centerY"), "Align Center Vertically");
+ addBtn("align-right", icons.right, () => this.align("right"), "Align Right");
+ addDivider();
+ addBtn("align-top", icons.top, () => this.align("top"), "Align Top");
+ addBtn("align-centerX", icons.centerX, () => this.align("centerX"), "Align Center Horizontally");
+ addBtn("align-bottom", icons.bottom, () => this.align("bottom"), "Align Bottom");
+ addDivider();
+ addBtn("equal-w", icons.equalW, () => this.equal("w"), "Equal Width");
+ addBtn("equal-h", icons.equalH, () => this.equal("h"), "Equal Height");
+ addDivider();
+ addBtn("dist-x", icons.distX, () => this.distribute("x"), "Horizontal Distribution");
+ addBtn("dist-y", icons.distY, () => this.distribute("y"), "Vertical Distribution");
+
+ this.insertionIndicator = document.createElement("div");
+ this.insertionIndicator.id = "nodealign-insert-indicator";
+ this.menu.appendChild(this.insertionIndicator);
+
+ this.toolbar.addEventListener("dblclick", () => this.resetPosition(false));
+
+ if (this.position.attached) {
+ const children = Array.from(this.menu.children).filter(c => c !== this.insertionIndicator);
+ const idx = Math.min(this.position.insertIndex, children.length);
+ this.menu.insertBefore(this.toolbar, children[idx] || null);
+ } else {
+ document.body.appendChild(this.toolbar);
+ }
+
+ document.addEventListener("mousemove", e => this.onDragging(e));
+ document.addEventListener("mouseup", () => this.onDragEnd());
+ document.addEventListener("selectstart", e => this.dragState.active && e.preventDefault());
+ },
+
+ adjustColor(hex, delta) {
+ const c = hex.padEnd(6, "0");
+ const r = parseInt(c.slice(0,2),16);
+ const g = parseInt(c.slice(2,4),16);
+ const b = parseInt(c.slice(4,6),16);
+ const bright = Math.round(0.299*r + 0.587*g + 0.114*b);
+ const adj = bright > 127 ? -delta : delta;
+ return `#${[r,g,b].map(v => Math.max(0, Math.min(255, v+adj)).toString(16).padStart(2,"0")).join("")}`;
+ },
+
+ getSelectedNodes() {
+ const pools = [app.canvas?.selected_nodes, app.graph?.canvas?.selected_nodes, app.graph?.selected_nodes];
+ const out = [];
+ for (const pool of pools) {
+ if (!pool) continue;
+ if (pool instanceof Map || pool instanceof Set) out.push(...pool.values());
+ else if (Array.isArray(pool)) out.push(...pool);
+ else if (typeof pool === "object") out.push(...Object.values(pool));
+ }
+ return out.filter(n => n && n.pos && n.size);
+ },
+
+ align(mode) {
+ const nodes = this.getSelectedNodes();
+ if (nodes.length < 2) return;
+ if (mode === "left") {
+ const minX = Math.min(...nodes.map(n => n.pos[0]));
+ nodes.forEach(n => n.pos[0] = minX);
+ } else if (mode === "right") {
+ const maxX = Math.max(...nodes.map(n => n.pos[0] + n.size[0]));
+ nodes.forEach(n => n.pos[0] = maxX - n.size[0]);
+ } else if (mode === "top") {
+ const minY = Math.min(...nodes.map(n => n.pos[1]));
+ nodes.forEach(n => n.pos[1] = minY);
+ } else if (mode === "bottom") {
+ const maxY = Math.max(...nodes.map(n => n.pos[1] + n.size[1]));
+ nodes.forEach(n => n.pos[1] = maxY - n.size[1]);
+ } else if (mode === "centerX") {
+ const minY = Math.min(...nodes.map(n => n.pos[1]));
+ const maxY = Math.max(...nodes.map(n => n.pos[1] + n.size[1]));
+ const c = (minY + maxY) / 2;
+ nodes.forEach(n => n.pos[1] = c - n.size[1] / 2);
+ } else if (mode === "centerY") {
+ const minX = Math.min(...nodes.map(n => n.pos[0]));
+ const maxX = Math.max(...nodes.map(n => n.pos[0] + n.size[0]));
+ const c = (minX + maxX) / 2;
+ nodes.forEach(n => n.pos[0] = c - n.size[0] / 2);
+ }
+ this.redraw();
+ },
+
+ equal(axis) {
+ const nodes = this.getSelectedNodes();
+ if (nodes.length < 2) return;
+ if (axis === "w") {
+ const w = nodes[0].size[0];
+ nodes.forEach(n => n.size[0] = w);
+ } else {
+ const h = nodes[0].size[1];
+ nodes.forEach(n => n.size[1] = h);
+ }
+ this.redraw();
+ },
+
+ distribute(axis) {
+ const nodes = this.getSelectedNodes();
+ if (nodes.length < 2) {
+ console.info("[NodeAlign] Distribution skipped: need 2+ nodes");
+ return;
+ }
+ const idx = axis === "x" ? 0 : 1;
+ const sorted = [...nodes].sort((a, b) => a.pos[idx] - b.pos[idx]);
+ const min = Math.min(...sorted.map(n => n.pos[idx]));
+ const max = Math.max(...sorted.map(n => n.pos[idx] + n.size[idx]));
+ const total = sorted.reduce((s, n) => s + n.size[idx], 0);
+ const gap = (max - min - total) / Math.max(1, sorted.length - 1);
+ let cursor = min;
+ sorted.forEach(n => {
+ n.pos[idx] = cursor;
+ cursor += n.size[idx] + gap;
+ });
+ console.info("[NodeAlign] Distribution", { axis, nodes: sorted.length, gap });
+ this.redraw();
+ },
+
+ redraw() {
+ if (app.canvas?.setDirty) app.canvas.setDirty(true, true);
+ if (app.graph?.setDirtyCanvas) app.graph.setDirtyCanvas(true);
+ },
+
+ updateDisplay() {
+ if (!this.toolbar) return;
+ const mode = this.getSetting("NodeAlign.NodeAlignDisplayMode", "permanent");
+ const selected = this.getSelectedNodes().length;
+ if (mode === "disabled") this.toolbar.style.display = "none";
+ else if (mode === "on-select" && selected < 2) this.toolbar.style.display = "none";
+ else this.toolbar.style.display = "flex";
+ },
+
+ queueDisplayUpdate() {
+ if (this.displayRaf) return;
+ this.displayRaf = requestAnimationFrame(() => {
+ this.displayRaf = null;
+ this.updateDisplay();
+ });
+ },
+
+ onDragStart(e) {
+ e.preventDefault();
+ const rect = this.toolbar.getBoundingClientRect();
+ this.dragState.active = true;
+ this.dragState.offsetX = e.clientX - rect.left;
+ this.dragState.offsetY = e.clientY - rect.top;
+ },
+
+ onDragging(e) {
+ if (!this.dragState.active) return;
+ const rect = this.toolbar.getBoundingClientRect();
+ const menuRect = this.menu.getBoundingClientRect();
+ const winW = window.innerWidth;
+ const winH = window.innerHeight;
+
+ if (this.position.attached) this.detach(true);
+
+ let left = e.clientX - this.dragState.offsetX;
+ let top = e.clientY - this.dragState.offsetY;
+ left = Math.max(0, Math.min(left, winW - rect.width));
+ top = Math.max(0, Math.min(top, winH - rect.height));
+ this.toolbar.style.left = `${left}px`;
+ this.toolbar.style.top = `${top}px`;
+
+ const tbBottom = rect.top + rect.height;
+ if (tbBottom > menuRect.top && rect.top < menuRect.bottom) {
+ this.insertionIndicator.style.display = "block";
+ const children = Array.from(this.menu.children).filter(c => c !== this.insertionIndicator && c !== this.toolbar);
+ const tbCenter = rect.left + rect.width / 2;
+ let indicatorLeft = 0;
+ for (let i = 0; i < children.length; i++) {
+ const childRect = children[i].getBoundingClientRect();
+ if (tbCenter < childRect.left + childRect.width / 2) {
+ indicatorLeft = childRect.left - menuRect.left;
+ break;
+ }
+ indicatorLeft = children[children.length - 1]?.getBoundingClientRect().right - menuRect.left || 0;
+ }
+ this.insertionIndicator.style.left = `${Math.max(0, Math.min(indicatorLeft, menuRect.width - this.insertionIndicator.offsetWidth))}px`;
+ } else {
+ this.insertionIndicator.style.display = "none";
+ }
+ },
+
+ onDragEnd() {
+ if (!this.dragState.active) return;
+ this.dragState.active = false;
+ const rect = this.toolbar.getBoundingClientRect();
+ const menuRect = this.menu.getBoundingClientRect();
+ const inMenu = rect.top + rect.height > menuRect.top && rect.top < menuRect.bottom;
+
+ if (inMenu) {
+ const children = Array.from(this.menu.children).filter(c => c !== this.insertionIndicator && c !== this.toolbar);
+ const tbCenter = rect.left + rect.width / 2;
+ let idx = 0;
+ for (let i = 0; i < children.length; i++) {
+ const childRect = children[i].getBoundingClientRect();
+ if (tbCenter < childRect.left + childRect.width / 2) { idx = i; break; }
+ idx = i + 1;
+ }
+ this.attach(children[idx]);
+ this.position.attached = true;
+ this.position.insertIndex = idx;
+ } else {
+ this.position.leftPct = ((rect.left + rect.width / 2) / window.innerWidth) * 100;
+ this.position.topPct = (rect.top / window.innerHeight) * 100;
+ this.position.attached = false;
+ this.position.insertIndex = 0;
+ this.updateFloatingPosition();
+ }
+
+ this.savePosition();
+ this.insertionIndicator.style.display = "none";
+ },
+
+ resetPosition(fromDivider) {
+ this.position = { leftPct: 50, topPct: 5, attached: false, insertIndex: 0 };
+ this.detach(true);
+ this.savePosition();
+ },
+
+ attach(target) {
+ this.position.attached = true;
+ this.toolbar.classList.remove("floating");
+ this.toolbar.classList.add("attached");
+ this.toolbar.style.left = "";
+ this.toolbar.style.top = "";
+ this.menu.insertBefore(this.toolbar, target || null);
+ },
+
+ detach(isDrag) {
+ if (!isDrag) return;
+ this.position.attached = false;
+ this.toolbar.classList.remove("attached");
+ this.toolbar.classList.add("floating");
+ document.body.appendChild(this.toolbar);
+ this.updateFloatingPosition();
+ },
+
+ updateFloatingPosition() {
+ if (!this.toolbar || this.position.attached) return;
+ const rect = this.toolbar.getBoundingClientRect();
+ const left = (this.position.leftPct / 100) * window.innerWidth - rect.width / 2;
+ const top = (this.position.topPct / 100) * window.innerHeight;
+ this.toolbar.style.left = `${Math.max(0, Math.min(left, window.innerWidth - rect.width))}px`;
+ this.toolbar.style.top = `${Math.max(0, Math.min(top, window.innerHeight - rect.height))}px`;
+ },
+
+ restorePosition() {
+ if (this.position.attached && this.menu && this.toolbar) {
+ const children = Array.from(this.menu.children).filter(c => c !== this.insertionIndicator && c !== this.toolbar);
+ const idx = Math.min(this.position.insertIndex, children.length);
+ this.attach(children[idx]);
+ } else {
+ this.position.attached = false;
+ this.updateFloatingPosition();
+ }
+ },
+
+ bindCanvasEvents() {
+ const refresh = () => this.queueDisplayUpdate();
+ this.canvasEl?.addEventListener("pointerup", refresh);
+ this.canvasEl?.addEventListener("click", refresh);
+ },
+
+ bindShortcuts() {
+ document.addEventListener("keydown", e => {
+ if (!e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
+ const enable = this.getSetting("NodeAlign.EnableAlignmentShortcuts", true);
+ if (!enable || this.getSelectedNodes().length < 2) return;
+ const key = e.key.toLowerCase();
+ if (["w","a","s","d"].includes(key)) e.preventDefault();
+ if (key === "w") this.align("top");
+ if (key === "a") this.align("left");
+ if (key === "s") this.align("bottom");
+ if (key === "d") this.align("right");
+ });
+ },
+
+ logCompat() {
+ const menuSel = MENU_SELECTORS.find(sel => document.querySelector(sel));
+ const canvasSel = CANVAS_SELECTORS.find(sel => document.querySelector(sel));
+ console.info("[NodeAlign] init", { menuSelector: menuSel, canvasSelector: canvasSel, rememberPosition: this.getSetting("NodeAlign.RememberPosition", true) });
+ },
+
+ applyLayoutSettings() {
+ if (!this.toolbar) return;
+ const size = Number(this.getSetting("NodeAlign.ButtonSize", 25)) || 25;
+ const dividerH = Math.round(size * 0.6);
+ const dividerW = Math.max(2, Math.round(size * 0.13));
+ const finalOrientation = "horizontal";
+ this.toolbar.style.setProperty("--nodealign-btn-size", `${size}px`);
+ this.toolbar.style.setProperty("--nodealign-divider-h", `${dividerH}px`);
+ this.toolbar.style.setProperty("--nodealign-divider-w", `${dividerW}px`);
+ console.info("[NodeAlign] layout", { size, orientation: finalOrientation });
+ }
+};
+
+app.registerExtension({
+ name: "NodeAlign.NodeAlignFull",
+ init() {
+ NodeAlignFull.init();
+ window.NodeAlignFull = NodeAlignFull;
+ }
+});
diff --git a/web/settings.js b/web/settings.js
new file mode 100644
index 0000000..b3ac501
--- /dev/null
+++ b/web/settings.js
@@ -0,0 +1,158 @@
+import { app } from "../../scripts/app.js";
+
+const nodeAlignSettings = [
+ {
+ id: "NodeAlign.NodeAlignDisplayMode",
+ name: "Node Alignment Toolbar Display Mode",
+ type: "combo",
+ options: [
+ { value: "permanent", text: "Permanent" },
+ { value: "on-select", text: "Show on Node Selection" },
+ { value: "disabled", text: "Disabled" }
+ ],
+ defaultValue: "permanent",
+ category: ["☲ NodeAlign", "NodeAlignment", "DisplayMode"],
+ onChange: (value) => {
+ if (window.KayNodeAlignmentManager) {
+ window.KayNodeAlignmentManager.updateDisplayMode(value);
+ }
+ if (window.NodeAlignFull) {
+ window.NodeAlignFull.updateDisplay();
+ }
+ }
+ },
+ // Remember position: now always on (no toggle)
+ {
+ id: "NodeAlign.EnableAlignmentShortcuts",
+ name: "Enable Shift+WASD for Quick Alignment (Up/Left/Down/Right)",
+ type: "boolean",
+ defaultValue: true,
+ category: ["☲ NodeAlign", "NodeAlignment", "Shortcuts"],
+ tooltip: "Quick align shortcut \nShift+W (Up ↑) \nShift+A (Left ←) \nShift+S (Down ↓) \nShift+D (Right →) \nalign selected nodes."
+ },
+ {
+ id: "NodeAlign.AlwaysOnTopMode",
+ name: "Toolbar Position Mode",
+ type: "combo",
+ options: [
+ { value: "always", text: "Float Above OEM (default)" },
+ { value: "attached", text: "Attach to OEM Bar" },
+ { value: "floating", text: "Free Float (normal z-index)" }
+ ],
+ defaultValue: "always",
+ category: ["☲ NodeAlign", "NodeAlignment", "Behavior"],
+ tooltip: "Choose how the toolbar sits: always above OEM (high z-index), attached to the OEM bar, or free-floating at normal z-index.",
+ onChange: () => {
+ if (window.NodeAlignFull?.applyAlwaysOnTop) window.NodeAlignFull.applyAlwaysOnTop();
+ }
+ },
+ {
+ id: "NodeAlign.ButtonSize",
+ name: "Toolbar Button Size (px)",
+ type: "slider",
+ defaultValue: 25,
+ attrs: { min: 18, max: 48, step: 1 },
+ category: ["☲ NodeAlign", "NodeAlignment", "Layout"],
+ onChange: () => {
+ if (window.NodeAlignFull) window.NodeAlignFull.applyLayoutSettings();
+ }
+ },
+ {
+ id: "NodeAlign.ResetPositionOnLoad",
+ name: "Always Reset Toolbar Position on Load",
+ type: "boolean",
+ defaultValue: false,
+ category: ["☲ NodeAlign", "NodeAlignment", "Behavior"],
+ tooltip: "If enabled, the toolbar position resets to default on each load instead of restoring the last position."
+ },
+ {
+ id: "NodeAlign.NodeAlignBackgroundOpacity",
+ name: "Node Alignment Toolbar Background Opacity",
+ type: "slider",
+ defaultValue: 0,
+ attrs: { min: 0, max: 100, step: 1 },
+ category: ["☲ NodeAlign", "NodeAlignment", "BackgroundOpacity"],
+ onChange: (newVal) => {
+ const ids = ['kay-node-alignment-toolbar', 'nodealign-full-toolbar'];
+ const bgColor = app.ui.settings.getSettingValue("NodeAlign.NodeAlignBackgroundColor") || "000000";
+ const opacity = newVal / 100;
+ ids.forEach(id => {
+ const toolbar = document.getElementById(id);
+ if (toolbar) {
+ toolbar.style.background = `rgba(${parseInt(bgColor.substr(0, 2), 16)}, ${parseInt(bgColor.substr(2, 2), 16)}, ${parseInt(bgColor.substr(4, 2), 16)}, ${opacity})`;
+ }
+ });
+ }
+ },
+ {
+ id: "NodeAlign.NodeAlignBackgroundColor",
+ name: "Node Alignment Toolbar Background Color",
+ type: "color",
+ defaultValue: "000000",
+ category: ["☲ NodeAlign", "NodeAlignment", "BackgroundColor"],
+ onChange: (newVal) => {
+ const ids = ['kay-node-alignment-toolbar', 'nodealign-full-toolbar'];
+ if (!newVal) return;
+ const opacity = app.ui.settings.getSettingValue("NodeAlign.NodeAlignBackgroundOpacity") / 100;
+ ids.forEach(id => {
+ const toolbar = document.getElementById(id);
+ if (toolbar) {
+ toolbar.style.background = `rgba(${parseInt(newVal.substr(0, 2), 16)}, ${parseInt(newVal.substr(2, 2), 16)}, ${parseInt(newVal.substr(4, 2), 16)}, ${opacity})`;
+ }
+ });
+ }
+ },
+ {
+ id: "NodeAlign.NodeAlignIconBackgroundColor",
+ name: "Node Alignment Icon Background Color",
+ type: "color",
+ defaultValue: "363636",
+ category: ["☲ NodeAlign", "NodeAlignment", "IconBackgroundColor"],
+ tooltip: "Use parameter “363636” to match ComfyUI menu bar color for an icon-free background effect",
+ onChange: (newVal) => {
+ const buttons = document.querySelectorAll('.kay-align-button, .nodealign-btn');
+ buttons.forEach(btn => btn.style.backgroundColor = `#${newVal}`);
+ const toolbar = document.getElementById('nodealign-full-toolbar');
+ if (toolbar) toolbar.style.setProperty('--nodealign-icon-bg', `#${newVal}`);
+ }
+ },
+ {
+ id: "NodeAlign.NodeAlignIconColor",
+ name: "Node Alignment Icon Color",
+ type: "color",
+ defaultValue: "999999",
+ category: ["☲ NodeAlign", "NodeAlignment", "IconColor"],
+ onChange: (newVal) => {
+ const buttons = document.querySelectorAll('.kay-align-button, .nodealign-btn');
+ buttons.forEach(btn => {
+ const svg = btn.querySelector('svg');
+ if (svg) svg.querySelectorAll('path').forEach(path => path.setAttribute('fill', `#${newVal}`));
+ });
+ }
+ },
+ {
+ id: "NodeAlign.NodeAlignDividerColor",
+ name: "Node Alignment Divider Color",
+ type: "color",
+ defaultValue: "00ff55",
+ category: ["☲ NodeAlign", "NodeAlignment", "DividerColor"],
+ onChange: (newVal) => {
+ const dividers = document.querySelectorAll('.kay-toolbar-divider, .nodealign-divider, #nodealign-insert-indicator');
+ dividers.forEach(divider => divider.style.background = `#${newVal}`);
+ }
+ }
+];
+
+app.registerExtension({
+ name: "NodeAlign.Settings",
+ settings: nodeAlignSettings,
+ setup() {
+ // Legacy bridge to ensure visibility in older UI builds
+ if (app.ui?.settings?.addSetting) {
+ nodeAlignSettings.forEach((setting) => {
+ try { app.ui.settings.addSetting(setting); } catch (err) {}
+ });
+ }
+ console.info("[NodeAlign.Settings] registered", nodeAlignSettings.map(s => s.id));
+ }
+});