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:
Greg
2026-05-24 12:14:41 -05:00
parent 1a84f8dd52
commit c60c42e7c0
2 changed files with 115 additions and 7 deletions
+77 -3
View File
@@ -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
View File
@@ -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; }