From 0861d7ed13956cd494c83a0860481ff9be21fcac Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 25 May 2026 05:06:26 -0500 Subject: [PATCH] v0.4.0: drag-to-reorder rows + custom order persisted to localStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets users define a workflow sequence (step 1 → step 2 → ...) by dragging rows in the panel. Order is independent of rgthree's auto-sort and persists across reloads. How it works: - Each row gets draggable="true" + HTML5 DnD handlers. - On drop, reorderGroup(moving, target, before|after) computes the new ordering, writes it to localStorage (ORDER_KEY), and re-renders. - Drop indicator: inset top/bottom box-shadow in the accent color showing where the dragged row will land. - sortByCustomOrder() applied at the end of getNavigableGroups so the matchColors filtering still happens first. - Groups not yet in the saved order keep relative order and end up after listed ones. New groups auto-append on the next reorder write. Shortcut badges follow the new order — 1..9 always map to the first 9 rows as they appear in the panel, so dragging a group to the top assigns it shortcut "1" (assuming no custom override). Settings panel gains two reset buttons: "Reset row order" (just the order) and "Reset all settings" (colors + shortcuts + order). --- web/index.js | 126 +++++++++++++++++++++++++++++++++++++++++++++---- web/styles.css | 18 +++++++ 2 files changed, 136 insertions(+), 8 deletions(-) diff --git a/web/index.js b/web/index.js index 9b496c5..aae2100 100644 --- a/web/index.js +++ b/web/index.js @@ -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.3.7"; +const CNV_VERSION = "0.4.0"; try { window.__cnv_version = CNV_VERSION; console.info(`[comfyui-navigator] loaded v${CNV_VERSION}`); @@ -19,6 +19,7 @@ const POS_KEY = "comfyui-navigator.panel-pos"; const COLLAPSE_KEY = "comfyui-navigator.collapsed"; const COLORS_KEY = "comfyui-navigator.colors"; const SHORTCUTS_KEY = "comfyui-navigator.shortcuts"; +const ORDER_KEY = "comfyui-navigator.group-order"; const COLOR_FIELDS = [ { key: "--cnv-bg", label: "Body background", default: "#0e283f" }, @@ -207,13 +208,76 @@ function getGroups() { * Fast Groups Muter, we mirror the *primary* one (= the muter with the most * managed widgets). That respects the user's matchColors partition: a sub- * group with a different color belongs to a smaller muter and gets filtered - * out of the nav list. If no muter exists, fall back to every group. */ + * out of the nav list. If no muter exists, fall back to every group. + * Finally, apply the user's custom drag-reorder (if set). */ function getNavigableGroups() { const all = getGroups(); const muters = getMutersAndWidgets(); - if (muters.length === 0) return all; - const primary = muters.reduce((a, b) => (a.widgets.size >= b.widgets.size ? a : b)); - return all.filter((g, i) => primary.widgets.has(groupTitle(g, i))); + let filtered; + if (muters.length === 0) { + filtered = all; + } else { + const primary = muters.reduce((a, b) => (a.widgets.size >= b.widgets.size ? a : b)); + filtered = all.filter((g, i) => primary.widgets.has(groupTitle(g, i))); + } + return sortByCustomOrder(filtered); +} + +function loadGroupOrder() { + try { + const v = localStorage.getItem(ORDER_KEY); + if (v) { + const arr = JSON.parse(v); + if (Array.isArray(arr)) return arr; + } + } catch {} + return null; +} +function saveGroupOrder(titles) { + try { + if (!titles || !titles.length) localStorage.removeItem(ORDER_KEY); + else localStorage.setItem(ORDER_KEY, JSON.stringify(titles)); + } catch {} +} + +/** Sort `groups` by the custom-order array. Unlisted groups keep their + * relative order and end up after listed ones. */ +function sortByCustomOrder(groups) { + const order = loadGroupOrder(); + if (!order) return groups; + const idx = new Map(); + order.forEach((t, i) => idx.set(t, i)); + return [...groups] + .map((g, i) => ({ g, origIdx: i, title: groupTitle(g, i) })) + .sort((a, b) => { + const ai = idx.has(a.title) ? idx.get(a.title) : Number.POSITIVE_INFINITY; + const bi = idx.has(b.title) ? idx.get(b.title) : Number.POSITIVE_INFINITY; + if (ai !== bi) return ai - bi; + return a.origIdx - b.origIdx; + }) + .map((x) => x.g); +} + +/** Move `movingTitle` to be right before/after `targetTitle` in the + * persisted custom order. */ +function reorderGroup(movingTitle, targetTitle, placement) { + // Build current order, seeding with the current navigable titles so that + // any group not yet in the saved order gets a stable slot. + const navTitles = getNavigableGroups().map((g, i) => groupTitle(g, i)); + let order = loadGroupOrder(); + if (!order) order = [...navTitles]; + // Drop anything no longer present so the stored order stays clean + order = order.filter((t) => navTitles.includes(t)); + // Append any new titles missing from order + for (const t of navTitles) if (!order.includes(t)) order.push(t); + + // Now actually move + order = order.filter((t) => t !== movingTitle); + const targetIdx = order.indexOf(targetTitle); + if (targetIdx < 0) order.push(movingTitle); + else order.splice(placement === "after" ? targetIdx + 1 : targetIdx, 0, movingTitle); + saveGroupOrder(order); + scheduleRender(); } function groupBounds(g) { @@ -588,7 +652,10 @@ function buildSettings(el) {
${shortcutRows}
- +
+ + +
`; // Color picker handlers @@ -624,13 +691,22 @@ function buildSettings(el) { persistShortcutsFromDom(el); }); - // Reset + // Reset row order only + el.querySelector("[data-reset-order]").addEventListener("click", () => { + if (!confirm("Reset row order to the default (rgthree's order)?")) return; + saveGroupOrder(null); + scheduleRender(); + }); + + // Reset everything el.querySelector("[data-reset]").addEventListener("click", () => { - if (!confirm("Reset all colors and shortcuts to defaults?")) return; + if (!confirm("Reset colors, shortcuts, and row order to defaults?")) return; try { localStorage.removeItem(COLORS_KEY); } catch {} try { localStorage.removeItem(SHORTCUTS_KEY); } catch {} + try { localStorage.removeItem(ORDER_KEY); } catch {} applyColors(state.panel); buildSettings(el); + scheduleRender(); }); } @@ -710,11 +786,45 @@ function renderList() { const row = document.createElement("div"); row.className = "cnv-row"; row.dataset.title = title; + row.draggable = true; const enabled = isGroupEnabled(title); const hasMuter = enabled !== null; if (enabled === true) row.classList.add("cnv-row--enabled"); + row.addEventListener("dragstart", (e) => { + state.dragTitle = title; + e.dataTransfer.effectAllowed = "move"; + try { e.dataTransfer.setData("text/plain", title); } catch {} + row.classList.add("cnv-row--dragging"); + }); + row.addEventListener("dragend", () => { + state.dragTitle = null; + row.classList.remove("cnv-row--dragging"); + state.listEl.querySelectorAll(".cnv-row--drop-above, .cnv-row--drop-below") + .forEach((r) => r.classList.remove("cnv-row--drop-above", "cnv-row--drop-below")); + }); + row.addEventListener("dragover", (e) => { + if (!state.dragTitle || state.dragTitle === title) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + const rect = row.getBoundingClientRect(); + const below = (e.clientY - rect.top) > rect.height / 2; + row.classList.toggle("cnv-row--drop-above", !below); + row.classList.toggle("cnv-row--drop-below", below); + }); + row.addEventListener("dragleave", () => { + row.classList.remove("cnv-row--drop-above", "cnv-row--drop-below"); + }); + row.addEventListener("drop", (e) => { + const moving = state.dragTitle; + if (!moving || moving === title) return; + e.preventDefault(); + const rect = row.getBoundingClientRect(); + const below = (e.clientY - rect.top) > rect.height / 2; + reorderGroup(moving, title, below ? "after" : "before"); + }); + const toggleHtml = hasMuter ? `${enabled ? "yes" : "no"}` : `—`; diff --git a/web/styles.css b/web/styles.css index d494b3f..92e43af 100644 --- a/web/styles.css +++ b/web/styles.css @@ -144,6 +144,24 @@ } .cnv-row--enabled:hover { background: var(--cnv-row-hover); } +/* Drag and drop visual feedback */ +.cnv-row[draggable="true"] { cursor: grab; } +.cnv-row[draggable="true"]:active { cursor: grabbing; } +.cnv-row--dragging { opacity: 0.4; } +.cnv-row--drop-above { + box-shadow: inset 0 3px 0 var(--cnv-accent); +} +.cnv-row--drop-below { + box-shadow: inset 0 -3px 0 var(--cnv-accent); +} +/* When a row is BOTH enabled AND a drop target, keep both visuals */ +.cnv-row--enabled.cnv-row--drop-above { + box-shadow: inset 2px 0 0 var(--cnv-accent), inset 0 3px 0 var(--cnv-accent); +} +.cnv-row--enabled.cnv-row--drop-below { + box-shadow: inset 2px 0 0 var(--cnv-accent), inset 0 -3px 0 var(--cnv-accent); +} + .cnv-row__title { flex: 1; white-space: nowrap;