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