Skip to content

Commit 771ea2c

Browse files
Code2Collapseclaude
andcommitted
feat(ui): port session UI work to main — WanDirector LTX rebuild + shared kit
Surgical port from feat/v2-ai-spine of this session's UI work (NO blind merge — main stays curated, AI-spine features excluded): - WanDirector: DOM track sidebar (pills/eye toggles), '+' add-menu, GLOBAL PROMPT, CSS design-system, game-sprite noodle completion FX. - Shared _c2c_ui_kit.js design system + rollout: points-bbox editor, video player, video-comparer pills. - Bookmark toggle moved Ctrl+Alt+B -> Alt+B (no build-workflow collision). - Vendored self-contained infra deps feat added (_c2c_lite, _editor_empty_state) so no dangling imports. Verified: whole-tree ./import check clean; every ported .js node --check OK. (spline_mask_editor intentionally NOT ported — its bg3 theme import would require changing the 34-importer _c2c_theme; left for a dedicated pass.) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 60f5adf commit 771ea2c

10 files changed

Lines changed: 2953 additions & 310 deletions

js/_c2c_lite.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// _c2c_lite.js — C2C "Lite / Performance mode".
2+
// ---------------------------------------------------------------------------
3+
// This pack ships 100+ JS extensions. On a busy graph (1000s of nodes) the
4+
// cumulative per-frame + per-event overhead of the *visual extras* (completion
5+
// FX, animated noodles, always-on HUD pills, per-node badges, mood board, etc.)
6+
// is what makes a loaded box feel sluggish/unresponsive. Lite mode lets the user
7+
// switch those OFF so only the functional tools remain.
8+
//
9+
// HOW IT WORKS (load-order-proof): the flag lives in localStorage and is read
10+
// SYNCHRONOUSLY at module-eval time, BEFORE any heavy extension registers. Each
11+
// gated extension does `import { LITE } from "./_c2c_lite.js"` — the ES import
12+
// guarantees this module evaluates first — and wraps its registration in
13+
// `if (!LITE) …`. So in lite mode the heavy extension never registers: its draw
14+
// hooks, DOM, and timers are never installed (true load reduction, not a flag
15+
// checked every frame).
16+
//
17+
// Toggle: Settings → C2C → Performance → "Lite mode". Changing it writes
18+
// localStorage and the value applies on the next page load (extensions are
19+
// imported once at startup), so we offer a one-click reload.
20+
21+
import { app } from "/scripts/app.js";
22+
23+
const LS_KEY = "c2c.lite";
24+
25+
export const LITE = (() => {
26+
try { return localStorage.getItem(LS_KEY) === "1"; } catch (_) { return false; }
27+
})();
28+
29+
// Optional helper for gated files that prefer a function call.
30+
export function liteSkip(label) {
31+
if (LITE && label) { try { console.debug(`[C2C.Lite] skipped ${label}`); } catch (_) {} }
32+
return LITE;
33+
}
34+
35+
// localStorage is the SOLE source of truth (read at module-eval). ComfyUI fires
36+
// the setting's onChange with its server-stored value during init, which must
37+
// NOT be allowed to clobber localStorage — so onChange is ignored until the user
38+
// can actually interact (after setup).
39+
let _initDone = false;
40+
41+
if (!(app.extensions || []).some((e) => e?.name === "C2C.LiteMode")) app.registerExtension({
42+
name: "C2C.LiteMode",
43+
settings: [
44+
{
45+
id: "c2c.lite.enabled",
46+
name: "Lite mode — disable C2C visual extras (FX, animated noodles, HUD pills, badges) for performance",
47+
tooltip: "Recommended on heavy graphs / low-RAM machines. Keeps all functional tools; turns off "
48+
+ "eye-candy and always-on overlays. Applies after a page reload.",
49+
type: "boolean",
50+
defaultValue: LITE,
51+
category: ["c2c", "Performance", "Lite mode"],
52+
onChange: (v) => {
53+
if (!_initDone) return; // ignore the init echo + our own sync (don't clobber localStorage)
54+
const on = !!v;
55+
let changed = false;
56+
try { changed = (localStorage.getItem(LS_KEY) === "1") !== on; localStorage.setItem(LS_KEY, on ? "1" : "0"); } catch (_) {}
57+
if (!changed) return;
58+
// Offer an immediate reload so the change takes effect.
59+
try {
60+
const t = app.extensionManager?.toast;
61+
if (t?.add) {
62+
t.add({ severity: "info", summary: "C2C Lite mode",
63+
detail: `Lite mode ${on ? "ON" : "OFF"} — reload to apply.`, life: 6000 });
64+
}
65+
} catch (_) {}
66+
// A gentle confirm to reload now (skips if the host blocks dialogs).
67+
setTimeout(() => {
68+
try {
69+
if (window.confirm(`C2C Lite mode ${on ? "enabled" : "disabled"}.\nReload now to apply?`)) {
70+
location.reload();
71+
}
72+
} catch (_) {}
73+
}, 50);
74+
},
75+
},
76+
],
77+
async setup() {
78+
// Sync the checkbox to the real (localStorage) state without writing back
79+
// (onChange is still gated by _initDone), then allow user toggles.
80+
try { app.ui.settings.setSettingValue("c2c.lite.enabled", LITE); } catch (_) {}
81+
setTimeout(() => { _initDone = true; }, 800);
82+
if (LITE) { try { console.log("%c[C2C.Lite] active — visual extras disabled for performance", "color:#8cf"); } catch (_) {} }
83+
},
84+
});

js/_c2c_ui_kit.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// _c2c_ui_kit.js — shared LTX-quality design system for C2C/MEC node UIs.
2+
//
3+
// WHY: our node editors used ad-hoc inline styles and read as "canvas
4+
// sketches" next to LTX Director, which ships one consistent stylesheet
5+
// (pill buttons, floating-label panels, popover menus, sub-status pills,
6+
// hover/active transitions, a disciplined neutral-dark palette). This module
7+
// is that stylesheet, injected once and shared by every node, so the whole
8+
// pack looks like one finished pro tool.
9+
//
10+
// USAGE:
11+
// import { ensureC2CKit } from "./_c2c_ui_kit.js";
12+
// ensureC2CKit(); // inject the stylesheet (idempotent)
13+
// root.classList.add("c2ck"); // opt a subtree into the system
14+
// btn.className = "c2ck-btn"; // then use the classes below
15+
//
16+
// The functional accent colours a node needs (skeleton=blue, iris=red, …)
17+
// stay the node's own business — the kit only styles the CHROME.
18+
//
19+
// License: Apache-2.0
20+
21+
const KIT_ID = "c2ck-styles";
22+
23+
const CSS = `
24+
.c2ck{--k-bg:#161616;--k-panel:#1e1e1e;--k-panel2:#222;--k-line:#111;--k-line2:#2c2c2c;
25+
--k-fg:#e6e6e6;--k-dim:#8a8a8a;--k-dim2:#666;--k-acc:#5b9dd9;
26+
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--k-fg);box-sizing:border-box;}
27+
.c2ck *,.c2ck *::before,.c2ck *::after{box-sizing:border-box;}
28+
29+
/* buttons */
30+
.c2ck-btn{background:var(--k-panel2);color:var(--k-fg);border:1px solid var(--k-line);border-radius:5px;
31+
padding:5px 11px;font-size:11px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:5px;
32+
transition:background .15s ease,border-color .15s ease,transform .05s ease;}
33+
.c2ck-btn:hover:not(:disabled){background:#2e2e2e;border-color:#4a4a4a;}
34+
.c2ck-btn:active:not(:disabled){transform:translateY(1px);}
35+
.c2ck-btn:disabled{opacity:.45;cursor:not-allowed;}
36+
.c2ck-btn.on{background:#1c2733;border-color:#2f4a63;color:#cfe6ff;}
37+
.c2ck-btn-danger:hover:not(:disabled){background:#3a1717;border-color:#a44;color:#ffb4b4;}
38+
.c2ck-btn-icon{padding:5px 8px;font-size:12px;justify-content:center;min-width:28px;}
39+
.c2ck-sep{width:1px;align-self:stretch;background:var(--k-line2);margin:2px 3px;}
40+
.c2ck-select{background:var(--k-panel2);color:var(--k-fg);border:1px solid var(--k-line);border-radius:5px;
41+
padding:4px 6px;font-size:11px;cursor:pointer;}
42+
43+
/* toolbars / status */
44+
.c2ck-toolbar{display:flex;gap:5px;align-items:center;flex-wrap:wrap;}
45+
.c2ck-status{font:11px ui-monospace,monospace;color:var(--k-dim);letter-spacing:.2px;}
46+
47+
/* panels + floating-label fields (the LTX .pr-prompt look) */
48+
.c2ck-panel{background:var(--k-panel);border:1px solid var(--k-line);border-radius:7px;position:relative;box-sizing:border-box;}
49+
.c2ck-fieldwrap{position:relative;width:100%;background:var(--k-panel);border:1px solid var(--k-line);border-radius:7px;
50+
overflow:hidden;transition:border-color .2s ease;}
51+
.c2ck-fieldwrap:focus-within{border-color:#4d6a86;}
52+
.c2ck-flabel{position:absolute;top:6px;left:9px;font-size:9px;font-weight:700;color:var(--k-dim2);
53+
text-transform:uppercase;letter-spacing:.6px;pointer-events:none;user-select:none;z-index:2;}
54+
.c2ck-fmeta{position:absolute;top:6px;right:9px;font:9px ui-monospace,monospace;color:var(--k-dim2);pointer-events:none;z-index:2;}
55+
.c2ck-area{width:100%;background:transparent;color:var(--k-fg);border:none;padding:22px 9px 9px;resize:none;
56+
font-size:12px;line-height:1.45;outline:none;box-sizing:border-box;}
57+
.c2ck-area::placeholder{color:#555;}
58+
.c2ck-info{background:#191919;color:#bcbcbc;border:1px solid var(--k-line);border-radius:7px;padding:10px 11px;
59+
font-size:11.5px;line-height:1.6;}
60+
.c2ck-info b,.c2ck-info .hi{color:#fff;font-weight:600;}
61+
.c2ck-itag{display:block;font-size:9px;font-weight:700;color:var(--k-dim2);text-transform:uppercase;
62+
letter-spacing:.6px;margin-bottom:6px;}
63+
64+
/* sub-status pill (LTX 'Inpaint: ON' / 'Audio: OFF') */
65+
.c2ck-pill{display:inline-flex;align-items:center;gap:4px;font-size:9px;font-weight:600;letter-spacing:.3px;
66+
padding:2px 7px;border-radius:999px;background:#242424;border:1px solid var(--k-line2);color:var(--k-dim);}
67+
.c2ck-pill.on{background:#12251a;border-color:#1f5b3a;color:#7fe0a6;}
68+
.c2ck-pill.off{background:#251414;border-color:#5b2020;color:#e08a8a;}
69+
70+
/* toggle chip */
71+
.c2ck-chip{font-size:10.5px;padding:3px 9px;border-radius:999px;cursor:pointer;border:1px solid var(--k-line2);
72+
background:transparent;color:var(--k-dim);transition:background .12s ease,border-color .12s ease,color .12s ease;}
73+
.c2ck-chip:hover{border-color:#555;}
74+
75+
/* popover menu (build per-open on document.body) */
76+
.c2ck-menu{position:fixed;z-index:2147483000;background:#1c1c1c;border:1px solid #333;border-radius:9px;padding:5px;
77+
box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;flex-direction:column;gap:1px;min-width:176px;}
78+
.c2ck-menu-head{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--k-dim2);padding:5px 10px 3px;}
79+
.c2ck-menu-btn{display:flex;align-items:center;gap:9px;background:none;border:none;color:var(--k-fg);
80+
font:12px ui-sans-serif,system-ui;text-align:left;padding:7px 10px;border-radius:6px;cursor:pointer;
81+
transition:background .12s ease;width:100%;box-sizing:border-box;}
82+
.c2ck-menu-btn:hover:not(:disabled){background:#2c2c2c;}
83+
.c2ck-menu-btn:disabled{opacity:.4;cursor:not-allowed;}
84+
.c2ck-menu-btn .g{width:16px;text-align:center;flex:0 0 auto;color:#9aa;}
85+
.c2ck-menu-sep{height:1px;background:#2c2c2c;margin:3px 4px;}
86+
87+
/* range inputs adopt the accent */
88+
.c2ck input[type=range]{accent-color:var(--k-acc);}
89+
`;
90+
91+
/** Inject the shared stylesheet once. Safe to call from every node's setup. */
92+
export function ensureC2CKit() {
93+
if (typeof document === "undefined") return;
94+
if (document.getElementById(KIT_ID)) return;
95+
const el = document.createElement("style");
96+
el.id = KIT_ID;
97+
el.textContent = CSS;
98+
document.head.appendChild(el);
99+
}
100+
101+
/**
102+
* Build a floating-label popover menu on document.body (eclipse-proof:
103+
* position:fixed, high z-index, dismiss on outside pointerdown/wheel). Returns
104+
* a `dismiss()` you should also call from the node's teardown.
105+
*
106+
* items: [{ label, glyph?, onPick, disabled?, title? } | { sep:true }]
107+
*/
108+
export function popoverMenu(clientX, clientY, { head, items } = {}) {
109+
ensureC2CKit();
110+
const menu = document.createElement("div");
111+
menu.className = "c2ck-menu";
112+
if (head) {
113+
const h = document.createElement("div");
114+
h.className = "c2ck-menu-head";
115+
h.textContent = head;
116+
menu.appendChild(h);
117+
}
118+
for (const it of (items || [])) {
119+
if (it.sep) { const s = document.createElement("div"); s.className = "c2ck-menu-sep"; menu.appendChild(s); continue; }
120+
const b = document.createElement("button");
121+
b.className = "c2ck-menu-btn";
122+
b.innerHTML = `<span class="g">${it.glyph || ""}</span>${it.label}`;
123+
if (it.disabled) { b.disabled = true; if (it.title) b.title = it.title; }
124+
else b.addEventListener("pointerdown", (ev) => { ev.preventDefault(); ev.stopPropagation(); dismiss(); try { it.onPick?.(); } catch (_) {} });
125+
menu.appendChild(b);
126+
}
127+
document.body.appendChild(menu);
128+
const vw = window.innerWidth, vh = window.innerHeight, r = menu.getBoundingClientRect();
129+
let px = clientX + 4, py = clientY - 6;
130+
if (px + r.width > vw - 6) px = vw - r.width - 6;
131+
if (py + r.height > vh - 6) py = vh - r.height - 6;
132+
menu.style.left = Math.max(6, px) + "px";
133+
menu.style.top = Math.max(6, py) + "px";
134+
let onDoc = null;
135+
function dismiss() {
136+
try { menu.remove(); } catch (_) {}
137+
if (onDoc) { document.removeEventListener("pointerdown", onDoc, true); document.removeEventListener("wheel", onDoc, true); onDoc = null; }
138+
}
139+
setTimeout(() => {
140+
onDoc = (ev) => { if (!menu.contains(ev.target)) dismiss(); };
141+
document.addEventListener("pointerdown", onDoc, true);
142+
document.addEventListener("wheel", onDoc, true);
143+
}, 0);
144+
return { menu, dismiss };
145+
}

js/_editor_empty_state.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// _editor_empty_state.js — shared empty-state painter for the MEC canvas
2+
// editors (spline / points+bbox / tracker / paint).
3+
//
4+
// An empty editor used to render as a flat near-black void — no grid, no
5+
// invitation, nothing that tells a first-time user what to do (the Director
6+
// timeline's "+ add image — or drop a file" lanes set the quality bar).
7+
// This paints, in DOC space (ctx already panned/zoomed):
8+
// 1. a subtle dark checkerboard over the canvas rect (reads as "empty
9+
// artboard", not "broken node"),
10+
// 2. a centered glyph + hint lines in muted slate.
11+
//
12+
// Colors are literal hex on purpose: canvas fillStyle cannot resolve
13+
// var(--x) (the all-black-confetti bug class).
14+
15+
const CHECK_A = "#15151d";
16+
const CHECK_B = "#1a1a24";
17+
18+
export function drawEditorEmptyState(ctx, w, h, z, glyph, lines) {
19+
const zz = Math.max(0.05, z || 1);
20+
ctx.save();
21+
// Checkerboard (doc-space 24px tiles, clipped to the canvas rect)
22+
ctx.beginPath();
23+
ctx.rect(0, 0, w, h);
24+
ctx.clip();
25+
const tile = 24;
26+
for (let y = 0; y < h; y += tile) {
27+
for (let x = 0; x < w; x += tile) {
28+
ctx.fillStyle = (((x + y) / tile) % 2 === 0) ? CHECK_A : CHECK_B;
29+
ctx.fillRect(x, y, Math.min(tile, w - x), Math.min(tile, h - y));
30+
}
31+
}
32+
// Centered glyph + hint lines (sized in screen px via 1/z)
33+
const cx = w / 2, cy = h / 2;
34+
ctx.textAlign = "center";
35+
ctx.textBaseline = "middle";
36+
ctx.fillStyle = "rgba(148,158,190,0.55)";
37+
ctx.font = `${28 / zz}px system-ui, sans-serif`;
38+
ctx.fillText(glyph || "✎", cx, cy - 26 / zz);
39+
ctx.font = `600 ${13 / zz}px system-ui, sans-serif`;
40+
ctx.fillStyle = "rgba(168,178,208,0.75)";
41+
if (lines && lines.length) ctx.fillText(lines[0], cx, cy + 4 / zz);
42+
ctx.font = `${11 / zz}px system-ui, sans-serif`;
43+
ctx.fillStyle = "rgba(128,138,166,0.55)";
44+
for (let i = 1; i < (lines?.length || 0); i++) {
45+
ctx.fillText(lines[i], cx, cy + (4 + i * 18) / zz);
46+
}
47+
ctx.restore();
48+
}

js/c2c_node_bookmarks.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
// c2c_node_bookmarks.js — Ctrl+B toggle bookmark, Ctrl+Shift+1..9 jump (C2C)
1+
// c2c_node_bookmarks.js — Alt+B toggle bookmark, Ctrl+Shift+1..9 jump (C2C)
22
// ---------------------------------------------------------------------
3+
// NOTE: This shortcut deliberately does NOT use plain Ctrl+B (ComfyUI core
4+
// binds it to "Bypass selected nodes") NOR Ctrl+Alt+B (the C2C AI workflow
5+
// builder / "build workflow" owns that). So the bookmark toggle is Alt+B.
6+
//
37
// What it does:
4-
// • Ctrl+B on a selected node → adds/removes a bookmark slot (1..9).
8+
// • Alt+B on a selected node → adds/removes a bookmark slot (1..9).
59
// Bookmark slot is auto-assigned to the lowest free index 1..9.
610
// • Ctrl+Shift+1..9 → pan + zoom to that bookmark, pulse the node.
711
// • Bookmarks persist in workflow JSON under
@@ -184,15 +188,24 @@ function isEditingField() {
184188

185189
function onKey(ev) {
186190
if (isEditingField()) return;
187-
if (!(ev.ctrlKey || ev.metaKey)) return;
188-
// Ctrl+B → toggle bookmark on selected node
189-
if ((ev.key === "b" || ev.key === "B") && !ev.shiftKey) {
191+
// Respect the on/off setting — switching it OFF must also disable the
192+
// keyboard shortcuts. (Previously only the strip was hidden in render();
193+
// this handler ignored the setting, so Ctrl+B still fired — the
194+
// "bookmark won't switch off" bug.)
195+
try {
196+
if (app.ui?.settings?.getSettingValue?.(SETTING_ID, true) === false) return;
197+
} catch { /* settings not ready */ }
198+
// Alt+B (NO Ctrl/Meta/Shift) → toggle bookmark on selected node.
199+
// Moved off Ctrl+Alt+B because that collided with the AI workflow builder
200+
// ("build workflow"). ev.code is layout-independent and unaffected by Alt
201+
// (Alt+b can change ev.key on some OSes).
202+
if (ev.altKey && !ev.ctrlKey && !ev.metaKey && !ev.shiftKey && ev.code === "KeyB") {
190203
ev.preventDefault();
191204
toggleBookmark();
192205
return;
193206
}
194207
// Ctrl+Shift+1..9 → jump to slot
195-
if (ev.shiftKey && /^[1-9]$/.test(ev.key)) {
208+
if ((ev.ctrlKey || ev.metaKey) && ev.shiftKey && /^[1-9]$/.test(ev.key)) {
196209
ev.preventDefault();
197210
jumpToSlot(ev.key);
198211
}
@@ -204,7 +217,7 @@ app.registerExtension({
204217
try {
205218
app.ui.settings.addSetting({
206219
id: SETTING_ID,
207-
name: "Node bookmarks strip (Ctrl+B, Ctrl+Shift+1..9)",
220+
name: "Node bookmarks strip (Alt+B, Ctrl+Shift+1..9)",
208221
type: "boolean", defaultValue: true,
209222
category: ["c2c", "Overlays", "Bookmarks"],
210223
onChange: render,

0 commit comments

Comments
 (0)