diff --git a/movie_editor/frontend/editor_settings.js b/movie_editor/frontend/editor_settings.js
index 1e8c551c..3074aef7 100644
--- a/movie_editor/frontend/editor_settings.js
+++ b/movie_editor/frontend/editor_settings.js
@@ -1,14 +1,11 @@
-// Editor Settings modal: per-browser editor preferences (autocomplete, anchor).
-// Opened from the Settings menu. Distinct from Engine settings (which configures
-// Studio / Chain Sampler behavior on the project).
+// Editor settings: per-browser editor preferences (autocomplete, anchor).
+// A section of the unified Settings window. Distinct from Engine settings
+// (which configures Studio / Chain Sampler behavior on the project).
(function () {
const { el } = window.dom;
const S = window.Store;
const API = window.MovieEditorAPI;
- let overlay = null;
- function close() { if (overlay) { overlay.remove(); overlay = null; } }
-
function labeled(text, control) {
const l = el("label", "field"); l.append(el("span", null, text)); l.append(control); return l;
}
@@ -155,17 +152,8 @@
return row;
}
- function open() {
- close();
- overlay = el("div", "modal-overlay");
- const box = el("div", "modal");
- const head = el("div", "modal-head");
- head.append(el("div", "modal-title", "Editor settings"));
- const hr = el("div", "modal-head-right");
- const x = el("button", "btn ghost tiny", "✕"); x.onclick = close;
- hr.append(x); head.append(hr); box.append(head);
-
- const content = el("div", "modal-content es-content");
+ function mount(body) {
+ const content = el("div", "es-content");
content.append(toggleRow(
"Prompt autocomplete",
"Suggest matching shortcuts while you type in the global prompt and scene prompts. Shows the trigger, its prompt, and category.",
@@ -175,11 +163,20 @@
"Text before the first split trigger is a shared anchor prepended to every scene. Turn off to make that leading text Scene 1 instead.",
"anchorEnabled"));
content.append(anchorGuideSection());
-
- box.append(content); overlay.append(box);
- overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
- document.body.append(overlay);
+ body.append(content);
}
- window.EditorSettingsModal = { open, close };
+ window.SettingsWindow.register({
+ id: "editor", group: "", title: "Editor",
+ subtitle: "Per-browser preferences — they apply to this browser, not the project.",
+ keywords: "autocomplete anchor prompt shortcuts i2v bypass guide preferences",
+ iconBg: "linear-gradient(180deg,#6aa9ff,#3b6fd9)",
+ icon: '',
+ mount,
+ });
+
+ window.EditorSettingsModal = {
+ open: () => window.SettingsWindow.open("editor"),
+ close: () => window.SettingsWindow.close(),
+ };
})();
diff --git a/movie_editor/frontend/engine_settings.js b/movie_editor/frontend/engine_settings.js
index 4d769fc6..292d28bb 100644
--- a/movie_editor/frontend/engine_settings.js
+++ b/movie_editor/frontend/engine_settings.js
@@ -1,12 +1,12 @@
-// Engine settings modal: Studio, Chain Sampler, continuity (moved out of Project inspector).
+// Engine settings: Studio, Chain Sampler, continuity (moved out of Project inspector).
+// A section of the unified Settings window.
(function () {
const { el, clear } = window.dom;
const S = window.Store;
- let overlay = null;
+ let _mounted = null; // { scroller, content }
let unsub = null;
let _editing = false;
- let _onKey = null;
const FOLD_KEY = "funpack_engine_fold_";
function foldOpen(id, def) {
@@ -599,44 +599,22 @@
}
function render() {
- if (!overlay) return;
- const content = overlay.querySelector(".modal-content");
+ if (!_mounted) return;
+ const { scroller, content } = _mounted;
if (_editing) {
if (shouldProtect(document.activeElement, content)) return;
_editing = false;
}
- const scrollTop = content.scrollTop;
+ const scrollTop = scroller.scrollTop;
clear(content);
renderContent(content, S.get());
- content.scrollTop = scrollTop;
+ scroller.scrollTop = scrollTop;
}
- function close() {
- if (unsub) { unsub(); unsub = null; }
- if (_onKey) { document.removeEventListener("keydown", _onKey); _onKey = null; }
- if (overlay) overlay.remove();
- overlay = null;
- _editing = false;
- }
-
- function open() {
- if (!S.get().project) return;
- if (overlay) { render(); return; }
-
- overlay = el("div", "modal-overlay");
- const modal = el("div", "modal modal-wide");
- const head = el("div", "modal-head");
- head.append(el("div", "modal-title", "Engine Settings"));
- const closeBtn = el("button", "btn ghost", "✕");
- closeBtn.onclick = close;
- const heRight = el("div", "modal-head-right"); heRight.append(closeBtn);
- head.append(heRight);
- modal.append(head);
- const content = el("div", "modal-content engine-modal-content");
- modal.append(content);
- overlay.append(modal);
- overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
- document.body.append(overlay);
+ function mount(body) {
+ const content = el("div", "engine-modal-content");
+ body.append(content);
+ _mounted = { scroller: body, content };
content.addEventListener("focusin", (e) => {
const t = e.target;
@@ -649,12 +627,27 @@
setTimeout(() => { if (!_editing) render(); }, 60);
});
- _onKey = (e) => { if (e.key === "Escape") close(); };
- document.addEventListener("keydown", _onKey);
-
unsub = S.subscribe(() => render());
render();
+ return () => {
+ if (unsub) { unsub(); unsub = null; }
+ _mounted = null;
+ _editing = false;
+ };
}
- window.EngineSettingsModal = { open, close };
+ window.SettingsWindow.register({
+ id: "engine", group: "Generation", order: 1, title: "Engine",
+ subtitle: "FunPack Studio, Chain Sampler, and continuity for the open project.",
+ keywords: "studio chain sampler refinement key seed cfg guidance continuity guides presets "
+ + "embed dynashift joyai decode transition overlap steer adjustments temporal",
+ iconBg: "linear-gradient(180deg,#ffb64d,#e07f1f)",
+ icon: '',
+ mount,
+ });
+
+ window.EngineSettingsModal = {
+ open: () => window.SettingsWindow.open("engine"),
+ close: () => window.SettingsWindow.close(),
+ };
})();
\ No newline at end of file
diff --git a/movie_editor/frontend/index.html b/movie_editor/frontend/index.html
index e623afeb..47061c3b 100644
--- a/movie_editor/frontend/index.html
+++ b/movie_editor/frontend/index.html
@@ -103,6 +103,7 @@
+
diff --git a/movie_editor/frontend/menubar.js b/movie_editor/frontend/menubar.js
index 2cd90010..78131d3f 100644
--- a/movie_editor/frontend/menubar.js
+++ b/movie_editor/frontend/menubar.js
@@ -219,6 +219,8 @@
} },
],
Settings: [
+ { label: "All Settings…", hint: "⌘,", action: () => window.SettingsWindow.open() },
+ { sep: true },
{ label: "Editor settings…", action: () => window.EditorSettingsModal.open() },
// Never grey this out over pipeline state: the modal itself explains WHY Studio /
// Chain Sampler knobs are unavailable (built-in pipeline disabled, custom slots)
@@ -363,4 +365,14 @@
});
S.subscribe((st) => { render(); renderChips(st); });
render();
+
+ // Single implementations for maintenance actions, shared with the Settings window
+ // (Refinement & Taste + Updates & ComfyUI sections) — menus stay as shortcuts.
+ window.FunPackMaintenance = {
+ exportRefinementKey,
+ importRefinementKeyFile: () => refinementKeyImportInput.click(),
+ deleteRefinementKey,
+ clearGlobalTaste,
+ restartComfy,
+ };
})();
diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js
index 8a87b346..445ab0d1 100644
--- a/movie_editor/frontend/models.js
+++ b/movie_editor/frontend/models.js
@@ -1,5 +1,6 @@
-// "Models" settings modal: configure pluggable node slots for the fixed pipeline.
+// "Models" settings section: configure pluggable node slots for the fixed pipeline.
// Pick a Model Type -> a Loader Node -> the editor exposes that node's inputs.
+// Lives in the unified Settings window; ModelsModal.open() deep-links to it.
(function () {
const { el, clear } = window.dom;
const API = window.MovieEditorAPI;
@@ -15,7 +16,7 @@
let coreProducers = []; // [{id,type,label}]
let requirements = []; // [{id,type,label,required,role_hint,hint}]
let wiringRules = {}; // guided wiring rules from /pipeline-ports
- let overlay = null;
+ let container = null; // mounted content root inside the Settings window
const expanded = new Set(); // slot ids currently expanded (collapsed by default)
let linkMode = false; // selecting inputs to bind into one shared control
let linkSel = []; // [{slotId, input, kind, choices, label}]
@@ -1095,10 +1096,9 @@
}
function render() {
- if (!overlay) return;
- const content = overlay.querySelector(".modal-content");
- clear(content);
- content.append(body());
+ if (!container) return;
+ clear(container);
+ container.append(body());
}
async function prewarmSpecs() {
@@ -1111,8 +1111,7 @@
render();
}
- async function open() {
- if (overlay) return; // already open — don't stack a second modal (orphans handlers)
+ async function loadAll() {
await ensureRoles();
try {
const pp = await API.pipelinePorts();
@@ -1129,23 +1128,42 @@
// metadata was captured, so opening Models once upgrades existing projects. persist()
// dispatches funpack-models-changed → the store reloads and the inspector re-renders.
if (refreshExposedChoices()) { try { await persist(); } catch (_) {} }
-
- overlay = el("div", "modal-overlay");
- const modal = el("div", "modal");
- const head = el("div", "modal-head");
- head.append(el("div", "modal-title", "Models & Pipeline Nodes"));
- const refresh = el("button", "btn ghost", "↻ Refresh model list"); refresh.onclick = refreshList;
- const closeModal = () => { if (overlay) overlay.remove(); overlay = null; };
- const close = el("button", "btn ghost", "✕"); close.onclick = closeModal;
- const heRight = el("div", "modal-head-right"); heRight.append(refresh); heRight.append(close);
- head.append(heRight);
- modal.append(head);
- modal.append(el("div", "modal-content"));
- overlay.append(modal);
- overlay.addEventListener("click", (e) => { if (overlay && e.target === overlay) closeModal(); });
- document.body.append(overlay);
- render();
}
- window.ModelsModal = { open, refresh: async () => { await ensureRoles().catch(() => {}); await doFullRefresh().catch(() => {}); } };
+ function mount(body, ctx) {
+ container = el("div", "models-mount");
+ container.append(el("div", "pj-meta", "Loading models & pipeline…"));
+ body.append(container);
+ const refresh = el("button", "btn ghost tiny", "↻ Refresh model list");
+ refresh.onclick = refreshList;
+ ctx.setActions([refresh]);
+ loadAll()
+ .then(() => { if (container && container.isConnected) render(); })
+ .catch(() => {
+ if (container && container.isConnected) {
+ clear(container);
+ container.append(el("div", "pj-meta", "Could not load models & pipeline."));
+ }
+ });
+ return () => { container = null; };
+ }
+
+ window.SettingsWindow.register({
+ id: "models", group: "Generation", order: 2, title: "Models & Pipeline",
+ subtitle: "Loaders and custom nodes wired into the fixed FunPack pipeline.",
+ keywords: "models loaders unet vae clip lora nodes pipeline wiring workflow import "
+ + "bypass disable core full control links",
+ iconBg: "linear-gradient(180deg,#b18cff,#7a4fd0)",
+ icon: '',
+ mount,
+ });
+
+ window.ModelsModal = {
+ open: () => window.SettingsWindow.open("models"),
+ refresh: async () => {
+ await ensureRoles().catch(() => {});
+ await doFullRefresh().catch(() => {});
+ if (container) render();
+ },
+ };
})();
diff --git a/movie_editor/frontend/settings_window.js b/movie_editor/frontend/settings_window.js
new file mode 100644
index 00000000..f3095e78
--- /dev/null
+++ b/movie_editor/frontend/settings_window.js
@@ -0,0 +1,247 @@
+// Unified Settings window, macOS System Settings style: a sidebar of grouped,
+// searchable sections (colored icon chips) + one content pane. Sections self-register
+// via SettingsWindow.register(); the legacy per-modal facades (EditorSettingsModal,
+// EngineSettingsModal, ModelsModal, TempBrowserModal) deep-link into their section,
+// so every existing entry point (menus, inspector, workflow import) keeps working.
+(function () {
+ const { el, clear } = window.dom;
+
+ const GROUPS = ["", "Generation", "Learning", "System"];
+ const registry = [];
+ let overlay = null, activeId = null, cleanup = null, query = "";
+ let navEl, bodyEl, titleEl, subEl, actionsEl;
+
+ function register(spec) {
+ registry.push(spec);
+ if (overlay) renderNav();
+ }
+
+ // Sidebar order: group order, then explicit spec.order, then registration order.
+ function orderedSections() {
+ const out = [];
+ GROUPS.forEach((g) => {
+ out.push(...registry
+ .filter((s) => (s.group || "") === g)
+ .sort((a, b) => (a.order ?? 99) - (b.order ?? 99)));
+ });
+ return out;
+ }
+
+ function matches(spec) {
+ if (!query) return true;
+ return (spec.title + " " + (spec.subtitle || "") + " " + (spec.keywords || ""))
+ .toLowerCase().includes(query);
+ }
+
+ function iconEl(spec) {
+ const w = el("span", "sw-icon");
+ if (spec.iconBg) w.style.background = spec.iconBg;
+ w.innerHTML = spec.icon || "";
+ return w;
+ }
+
+ function renderNav() {
+ clear(navEl);
+ const ordered = orderedSections();
+ GROUPS.forEach((g) => {
+ const items = ordered.filter((s) => (s.group || "") === g && matches(s));
+ if (!items.length) return;
+ if (g) navEl.append(el("div", "sw-group-title", g));
+ items.forEach((s) => {
+ const it = el("div", "sw-item" + (s.id === activeId ? " active" : ""));
+ it.append(iconEl(s), el("span", "sw-item-label", s.title));
+ it.onclick = () => show(s.id);
+ navEl.append(it);
+ });
+ });
+ if (!navEl.children.length) navEl.append(el("div", "sw-nav-empty", "No matches"));
+ }
+
+ function teardownSection() {
+ if (typeof cleanup === "function") { try { cleanup(); } catch (_) {} }
+ cleanup = null;
+ }
+
+ function show(id) {
+ const spec = registry.find((s) => s.id === id) || orderedSections()[0];
+ if (!spec || !overlay) return;
+ teardownSection();
+ activeId = spec.id;
+ titleEl.textContent = spec.title;
+ subEl.textContent = spec.subtitle || "";
+ subEl.hidden = !spec.subtitle;
+ clear(actionsEl);
+ clear(bodyEl);
+ bodyEl.scrollTop = 0;
+ const ctx = {
+ setActions: (nodes) => { clear(actionsEl); (nodes || []).forEach((n) => actionsEl.append(n)); },
+ openSection: (sid) => show(sid),
+ };
+ cleanup = spec.mount(bodyEl, ctx) || null;
+ renderNav();
+ }
+
+ function close() {
+ teardownSection();
+ if (overlay) overlay.remove();
+ overlay = null; activeId = null;
+ }
+
+ function open(id) {
+ // Recover if something removed our overlay from the DOM directly
+ // (e.g. the tour's closeModalOverlay sweep) without calling close().
+ if (overlay && !overlay.isConnected) { close(); }
+ if (overlay) { show(id || activeId); return; }
+ query = "";
+
+ overlay = el("div", "modal-overlay sw-overlay");
+ const win = el("div", "settings-win");
+
+ const side = el("div", "sw-side");
+ const search = el("input", "sw-search");
+ search.type = "search"; search.placeholder = "Search"; search.autocomplete = "off";
+ search.oninput = () => { query = search.value.trim().toLowerCase(); renderNav(); };
+ navEl = el("div", "sw-nav");
+ side.append(search, navEl);
+
+ const main = el("div", "sw-main");
+ const head = el("div", "sw-head");
+ const ht = el("div", "sw-head-text");
+ titleEl = el("div", "sw-head-title");
+ subEl = el("div", "sw-head-sub");
+ ht.append(titleEl, subEl);
+ actionsEl = el("div", "sw-head-actions");
+ const x = el("button", "btn ghost tiny sw-close", "✕");
+ x.onclick = close;
+ head.append(ht, actionsEl, x);
+ bodyEl = el("div", "sw-body");
+ main.append(head, bodyEl);
+
+ win.append(side, main);
+ overlay.append(win);
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
+ document.body.append(overlay);
+ show(id);
+ }
+
+ window.addEventListener("keydown", (e) => {
+ if (overlay && e.key === "Escape") { close(); return; }
+ if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key === ",") {
+ e.preventDefault();
+ overlay ? close() : open();
+ }
+ });
+
+ window.SettingsWindow = { open, close, register };
+
+ // ── shared row builders for settings panels ────────────────────────────
+ function actionRow(title, hint, btnLabel, onClick, opts = {}) {
+ const row = el("div", "sw-row");
+ const main = el("div", "sw-row-main");
+ main.append(el("div", "sw-row-title", title));
+ if (hint) main.append(el("div", "sw-row-hint", hint));
+ row.append(main);
+ const btn = el("button", "btn ghost tiny" + (opts.danger ? " danger" : ""), btnLabel);
+ btn.type = "button";
+ if (opts.disabled) btn.disabled = true;
+ btn.onclick = onClick;
+ row.append(btn);
+ return row;
+ }
+
+ function infoRow(title, value, ok) {
+ const row = el("div", "sw-row");
+ const main = el("div", "sw-row-main");
+ main.append(el("div", "sw-row-title", title));
+ row.append(main);
+ const val = el("span", "sw-row-value", value);
+ if (ok != null) {
+ const led = el("span", "sw-led " + (ok ? "ok" : "bad"));
+ row.append(led);
+ }
+ row.append(val);
+ return row;
+ }
+
+ // ── built-in section: Refinement & Taste (Learning) ────────────────────
+ // Buttons reuse the single implementations exported by menubar.js
+ // (window.FunPackMaintenance) — no duplicated logic, just a second door.
+ register({
+ id: "refinement", group: "Learning", title: "Refinement & Taste",
+ subtitle: "Learned-taste state: refinement keys and the Absolute global-taste store.",
+ keywords: "refinement key export import delete clear absolute taste store learning ratings",
+ iconBg: "linear-gradient(180deg,#ff8bc2,#d84f92)",
+ icon: '',
+ mount(body) {
+ const M = window.FunPackMaintenance || {};
+ body.append(el("div", "sw-hint",
+ "Refinement keys hold what FunPack has learned from your ratings for a named session. "
+ + "They are stored on this ComfyUI instance as .json plus sidecar banks."));
+ const rows = el("div", "sw-rows");
+ rows.append(actionRow("Export refinement key", "Download a key as .json.", "⬇ Export…", () => M.exportRefinementKey?.()));
+ rows.append(actionRow("Import refinement key", "Load a previously exported .json onto this instance.", "Import…", () => M.importRefinementKeyFile?.()));
+ body.append(rows);
+ body.append(el("div", "sw-rows-label", "Danger zone"));
+ const danger = el("div", "sw-rows");
+ danger.append(actionRow("Delete refinement key",
+ "Removes a key AND all its sidecars (value function, blessed banks, creativity latent, velocity memory).",
+ "Delete…", () => M.deleteRefinementKey?.(), { danger: true }));
+ danger.append(actionRow("Clear global taste store",
+ "Wipes the Absolute store pooled from every rated run (applied in absolute/both steer mode).",
+ "Clear…", () => M.clearGlobalTaste?.(), { danger: true }));
+ body.append(danger);
+ },
+ });
+
+ // ── built-in section: Updates & ComfyUI (System) ───────────────────────
+ register({
+ id: "system", group: "System", order: 1, title: "Updates & ComfyUI",
+ subtitle: "Server connection, FunPack code updates, pipeline health.",
+ keywords: "update git branch restart comfyui server connection health template version",
+ iconBg: "linear-gradient(180deg,#a7adb8,#6b7280)",
+ icon: '',
+ mount(body) {
+ const S = window.Store, G = window.FunPackGit, M = window.FunPackMaintenance || {};
+ const wrap = el("div", "sw-stack");
+ body.append(wrap);
+
+ function render() {
+ clear(wrap);
+ const st = S ? S.get() : {};
+ const git = G ? G.get() : null;
+ const gitOk = !!(git && git.ok);
+
+ wrap.append(el("div", "sw-rows-label", "ComfyUI server"));
+ const srv = el("div", "sw-rows");
+ srv.append(infoRow("Connection", st.health?.ok ? "Connected · " + window.location.host : "Not reachable", !!st.health?.ok));
+ srv.append(actionRow("Open ComfyUI", "The node-graph UI, in a new tab.", "↗ Open", () => window.open("/", "_blank")));
+ srv.append(actionRow("Restart ComfyUI", "Down for ~10–40s; any running generation is lost. Reloads automatically.", "⟳ Restart…", () => M.restartComfy?.(), { danger: true }));
+ wrap.append(srv);
+
+ wrap.append(el("div", "sw-rows-label", "FunPack code"));
+ const code = el("div", "sw-rows");
+ code.append(infoRow("Branch", gitOk
+ ? git.branch + (git.commit ? " @ " + git.commit : "") + (git.dirty ? " (local changes)" : "")
+ : (git?.detail || "unavailable")));
+ code.append(actionRow("Switch branch", "Check out another FunPack branch and reload.", "Switch…",
+ () => G?.switchBranch?.(), { disabled: !gitOk || !!git?.dirty }));
+ code.append(actionRow("Update FunPack",
+ gitOk && git.behind > 0 ? git.behind + " new commit(s) available." : "Pull the latest code and reload.",
+ gitOk && git.behind > 0 ? git.behind + "↓ Update" : "⬇⟳ Update",
+ () => G?.update?.(), { disabled: !gitOk || !!git?.dirty }));
+ wrap.append(code);
+
+ wrap.append(el("div", "sw-rows-label", "Pipeline health"));
+ const health = el("div", "sw-rows");
+ health.append(infoRow("Workflow template", st.health?.reference_loaded ? "loaded" : "missing", !!st.health?.reference_loaded));
+ health.append(infoRow("Configured nodes", String(st.health?.configured_slots ?? 0)));
+ wrap.append(health);
+ }
+
+ const unsub = S ? S.subscribe(render) : null;
+ render();
+ if (G?.refresh) G.refresh().then(() => { if (wrap.isConnected) render(); }).catch(() => {});
+ return () => { if (unsub) unsub(); };
+ },
+ });
+})();
diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css
index b3e3b42a..42d97d56 100644
--- a/movie_editor/frontend/styles.css
+++ b/movie_editor/frontend/styles.css
@@ -1571,3 +1571,58 @@ body.tour-mode { overflow: hidden; }
.sc-pick-prompt { color: var(--muted); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
.sc-pick-count { margin-left: auto; color: var(--faint); font-family: var(--font-mono); font-size: 10.5px; flex-shrink: 0; }
+
+/* ── unified Settings window (macOS System Settings style) ─────────────── */
+.sw-overlay { align-items: center; padding: 4vh 16px; }
+.settings-win { width: min(960px, 96vw); height: min(660px, 90vh); display: flex;
+ background: #16140f; border: 1px solid var(--line); border-radius: 14px;
+ box-shadow: 0 24px 64px rgba(0,0,0,.55); overflow: hidden; animation: pop .16s ease; }
+
+.sw-side { width: 218px; flex: 0 0 auto; display: flex; flex-direction: column; gap: 10px;
+ padding: 12px 10px; background: #121009; border-right: 1px solid var(--line); }
+.sw-search { width: 100%; padding: 6px 9px; font: 12.5px var(--font-ui); color: var(--text);
+ background: var(--ink-2); border: 1px solid var(--line); border-radius: 8px; outline: none; }
+.sw-search:focus { border-color: var(--amber); }
+.sw-nav { flex: 1; overflow: auto; display: flex; flex-direction: column; gap: 2px; }
+.sw-group-title { margin: 10px 6px 3px; font-size: 10px; font-weight: 700; letter-spacing: .8px;
+ text-transform: uppercase; color: var(--faint); }
+.sw-nav-empty { padding: 12px 8px; font-size: 12px; color: var(--faint); }
+.sw-item { display: flex; align-items: center; gap: 9px; padding: 5px 8px; border-radius: 7px;
+ cursor: pointer; font-size: 13px; color: var(--text); user-select: none; }
+.sw-item:hover { background: var(--ink-3); }
+.sw-item.active { background: linear-gradient(180deg, var(--amber-2), var(--amber)); color: #241503; font-weight: 600; }
+.sw-icon { width: 23px; height: 23px; flex: 0 0 auto; border-radius: 6px; display: grid; place-items: center;
+ background: linear-gradient(180deg,#8a8f98,#5b6068);
+ box-shadow: inset 0 0 0 .5px rgba(255,255,255,.22), 0 1px 2px rgba(0,0,0,.4); }
+.sw-icon svg { display: block; }
+.sw-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.sw-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
+.sw-head { display: flex; align-items: center; gap: 12px; padding: 13px 18px 11px;
+ border-bottom: 1px solid var(--line); background: linear-gradient(#1b1813, #161410); }
+.sw-head-text { flex: 1; min-width: 0; }
+.sw-head-title { font-family: var(--font-display); font-weight: 700; font-size: 17px; letter-spacing: -.01em; }
+.sw-head-sub { margin-top: 1px; font-size: 11.5px; color: var(--muted); overflow: hidden;
+ text-overflow: ellipsis; white-space: nowrap; }
+.sw-head-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
+.sw-close { flex: 0 0 auto; }
+.sw-body { flex: 1; overflow: auto; padding: 16px 18px 24px; }
+
+/* grouped macOS-style rows (Refinement & Taste, Updates & ComfyUI) */
+.sw-stack { display: flex; flex-direction: column; }
+.sw-hint { font-size: 11.5px; line-height: 1.5; color: var(--muted); margin-bottom: 12px; }
+.sw-rows-label { margin: 16px 4px 5px; font-size: 10.5px; font-weight: 700; letter-spacing: .6px;
+ text-transform: uppercase; color: var(--faint); }
+.sw-rows { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 10px;
+ background: var(--ink-2); overflow: hidden; }
+.sw-row { display: flex; align-items: center; gap: 12px; padding: 9px 12px; border-top: 1px solid var(--line); }
+.sw-row:first-child { border-top: none; }
+.sw-row-main { flex: 1; min-width: 0; }
+.sw-row-title { font-size: 13px; color: var(--text); }
+.sw-row-hint { margin-top: 1px; font-size: 11px; line-height: 1.4; color: var(--muted); }
+.sw-row-value { flex: 0 0 auto; max-width: 45%; font-family: var(--font-mono); font-size: 11.5px;
+ color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.sw-row .btn { flex: 0 0 auto; }
+.sw-led { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; }
+.sw-led.ok { background: var(--good); box-shadow: 0 0 6px var(--good); }
+.sw-led.bad { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
diff --git a/movie_editor/frontend/temp_browser.js b/movie_editor/frontend/temp_browser.js
index ad31d044..bce3240f 100644
--- a/movie_editor/frontend/temp_browser.js
+++ b/movie_editor/frontend/temp_browser.js
@@ -1,11 +1,11 @@
// Temp files browser: a lightweight media-bin view over ComfyUI's temp directory
-// (scene previews & other transient outputs that are wiped on restart). Opened from the
-// Settings menu. Each item can be opened in a new tab or saved to disk — no in-app player.
+// (scene previews & other transient outputs that are wiped on restart). A section of
+// the unified Settings window. Each item can be opened in a new tab or saved to disk
+// — no in-app player.
(function () {
const { el, clear } = window.dom;
const API = window.MovieEditorAPI;
- let overlay = null;
let files = [];
// Probe