v0.1.0: redesign as floating muter mirror
Each row in the panel is now a real two-way toggle wired to the corresponding widget on rgthree's Fast Groups Muter. Click the checkbox in the panel → muter widget flips. Toggle the muter widget directly on the canvas → panel checkbox catches up on the next poll (600ms). Save + load works because all state still lives in rgthree. Per row layout: [checkbox] [color dot] Group Name [shortcut N] [→ jump] [⋯ menu] Click body of row = jump. Explicit → button = same jump (visible affordance). ⋯ opens the same context menu as right-click. Groups with no managing muter render a dashed placeholder where the checkbox would be (visual signal: "this group is unmanaged"). Kept: drag-to-reposition, collapse, search, matchColors filter (only groups in the primary muter render), keyboard 1-9 = jump, version marker, all rgthree integration (single-muter Solo, all-muter Reset). Bumped to v0.1.0 since the row contract changed.
This commit is contained in:
+77
-3
@@ -7,7 +7,7 @@ import { app } from "../../scripts/app.js";
|
||||
// Bump on every release. Exposed on window so it's trivial to check in the
|
||||
// console (`window.__cnv_version`) whether the browser is on the latest JS
|
||||
// or still serving a cached older copy.
|
||||
const CNV_VERSION = "0.0.6";
|
||||
const CNV_VERSION = "0.1.0";
|
||||
try {
|
||||
window.__cnv_version = CNV_VERSION;
|
||||
console.info(`[comfyui-navigator] loaded v${CNV_VERSION}`);
|
||||
@@ -220,6 +220,25 @@ function findMuterOwningGroup(targetTitle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMuterWidgetForGroup(title) {
|
||||
const owner = findMuterOwningGroup(title);
|
||||
return owner ? owner.widgets.get(title) : null;
|
||||
}
|
||||
|
||||
function isGroupEnabled(title) {
|
||||
const w = getMuterWidgetForGroup(title);
|
||||
return w ? !!w?.value?.toggled : null; // null = no muter manages it
|
||||
}
|
||||
|
||||
function setGroupEnabled(title, on) {
|
||||
const w = getMuterWidgetForGroup(title);
|
||||
if (!w) return false;
|
||||
setMuterWidget(w, on);
|
||||
const owner = findMuterOwningGroup(title);
|
||||
owner?.muter?.setDirtyCanvas?.(true, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setMuterWidget(widget, toggled) {
|
||||
// Value is { toggled: bool }. Mutate in place AND call callback so rgthree
|
||||
// re-renders + dispatches its own change handlers.
|
||||
@@ -438,25 +457,77 @@ function renderList() {
|
||||
const row = document.createElement("div");
|
||||
row.className = "cnv-row";
|
||||
row.style.setProperty("--cnv-color", groupColorOrDefault(g));
|
||||
row.dataset.title = title;
|
||||
|
||||
const enabled = isGroupEnabled(title);
|
||||
const showCheckbox = enabled !== null; // null = no muter manages this group
|
||||
|
||||
row.innerHTML = `
|
||||
${showCheckbox ? `<input type="checkbox" class="cnv-row__cb" ${enabled ? "checked" : ""} title="Toggle whether this group runs">` : `<span class="cnv-row__cb cnv-row__cb--placeholder" title="No muter manages this group"></span>`}
|
||||
<span class="cnv-row__dot"></span>
|
||||
<span class="cnv-row__title"></span>
|
||||
${i < 9 ? `<span class="cnv-row__shortcut">${i + 1}</span>` : ""}
|
||||
<button type="button" class="cnv-row__jump" title="Jump to this group">→</button>
|
||||
<button type="button" class="cnv-row__menu" title="More actions">⋯</button>
|
||||
`;
|
||||
row.querySelector(".cnv-row__title").textContent = title;
|
||||
|
||||
// Checkbox: toggle muter widget (don't jump)
|
||||
const cb = row.querySelector("input.cnv-row__cb");
|
||||
if (cb) {
|
||||
cb.addEventListener("click", (e) => e.stopPropagation());
|
||||
cb.addEventListener("change", (e) => {
|
||||
setGroupEnabled(title, cb.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// Click the row body (anywhere except checkbox/jump/menu) → jump
|
||||
row.addEventListener("click", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
if (e.target.closest(".cnv-row__cb, .cnv-row__menu")) return;
|
||||
jumpToGroup(g);
|
||||
});
|
||||
|
||||
// Explicit jump button (visible affordance)
|
||||
row.querySelector(".cnv-row__jump").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
jumpToGroup(g);
|
||||
});
|
||||
|
||||
// Three-dot menu
|
||||
row.querySelector(".cnv-row__menu").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openRowMenu(g, e);
|
||||
});
|
||||
|
||||
// Right-click anywhere on row also opens menu
|
||||
row.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openRowMenu(g, e);
|
||||
});
|
||||
|
||||
state.listEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
/** Cheap re-sync of just the checkbox states without rebuilding the list.
|
||||
* Catches the case where the user toggles a widget directly on the muter
|
||||
* node (or via rgthree's keyboard shortcuts) and we need to reflect it in
|
||||
* the panel. */
|
||||
function syncCheckboxStates() {
|
||||
if (!state.listEl) return;
|
||||
for (const row of state.listEl.querySelectorAll(".cnv-row")) {
|
||||
const title = row.dataset.title;
|
||||
const cb = row.querySelector("input.cnv-row__cb");
|
||||
if (!cb) continue;
|
||||
const enabled = isGroupEnabled(title);
|
||||
if (enabled === null) continue;
|
||||
if (cb.checked !== enabled) cb.checked = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// --- keyboard hotkeys ---------------------------------------------------
|
||||
|
||||
function isTypingTarget(el) {
|
||||
@@ -509,15 +580,18 @@ app.registerExtension({
|
||||
|
||||
// Catch graph swaps (loading a different workflow). ComfyUI fires
|
||||
// a custom "graphConfigured" or similar; polling _groups length as
|
||||
// a cheap fallback.
|
||||
// a cheap fallback. Also re-sync checkbox states so that toggles
|
||||
// made directly on the muter node propagate to the panel UI.
|
||||
let lastGroupCount = -1;
|
||||
setInterval(() => {
|
||||
const n = (app.graph?._groups?.length || 0);
|
||||
if (n !== lastGroupCount) {
|
||||
lastGroupCount = n;
|
||||
scheduleRender();
|
||||
} else {
|
||||
syncCheckboxStates();
|
||||
}
|
||||
}, 800);
|
||||
}, 600);
|
||||
},
|
||||
async loadedGraphNode() {
|
||||
scheduleRender();
|
||||
|
||||
+38
-4
@@ -85,8 +85,8 @@
|
||||
.cnv-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid var(--cnv-color, #3b82f6);
|
||||
@@ -94,9 +94,26 @@
|
||||
.cnv-row:hover { background: rgba(255, 255, 255, 0.06); }
|
||||
.cnv-row--active { background: rgba(96, 165, 250, 0.14); }
|
||||
|
||||
.cnv-row__cb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--cnv-color, #3b82f6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cnv-row__cb--placeholder {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.15);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cnv-row__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--cnv-color, #3b82f6);
|
||||
flex-shrink: 0;
|
||||
@@ -118,5 +135,22 @@
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.cnv-row__jump,
|
||||
.cnv-row__menu {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
.cnv-row__jump:hover,
|
||||
.cnv-row__menu:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.cnv-panel--collapsed .cnv-panel__search-wrap,
|
||||
.cnv-panel--collapsed .cnv-panel__list { display: none; }
|
||||
|
||||
Reference in New Issue
Block a user