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)); + } +});