feat(ui): Settings window as v4 laid it out — grouped sections, About and Appearance real, the rest named stand-ins

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-03 21:54:14 +03:00
co-authored by Claude Sonnet 5.5
parent 63dfd9c499
commit 326f3a145a
10 changed files with 80 additions and 5 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "o
const frame = build(document.getElementById("app"));
const selection = createSelection({ project });
selection.on(() => say("select"));
const app = { project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead() };
const app = { project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme };
const settings = createSettings({ menubar: hostFor("menubar.menus") });
const result = await loadFeatures(paths, { app });
settings.addButton();
+1
View File
@@ -56,6 +56,7 @@
background: transparent; color: var(--text);
font-size: var(--fs-5); text-align: left; cursor: pointer; width: 100%;
}
.cx-filter-gap { block-size: var(--sp-5); }
.cx-filter-row:hover { background: var(--hover); }
.cx-filter-row.cx-on { background: var(--accent-soft); color: var(--accent-hi); }
.cx-filter-row.cx-on .cx-filter-label { font-weight: var(--fw-semibold); }
+3
View File
@@ -169,7 +169,10 @@ define("filterList", "md", ({ items = [], value, onChange, placeholder = "Search
!q || `${i.label} ${i.hint || ""} ${i.keywords || ""}`.toLowerCase().includes(q));
list.replaceChildren();
if (!shown.length) { list.append(el("div", { cls: "cx-filter-empty", text: empty })); return; }
let before;
for (const item of shown) {
if (before !== undefined && item.group !== before) list.append(el("div", { cls: "cx-filter-gap", attrs: { "aria-hidden": "true" } })); // a new group starts after a gap
before = item.group;
// `tone` picks one of a FIXED set of chip colours -- never a raw
// colour from data, which is the rule `el()`'s style refusal exists to
// hold everywhere else too. Enforced, not just claimed: an unknown
+26
View File
@@ -0,0 +1,26 @@
// About FunPack: which build this is, and the machine ComfyUI runs on (which on a rental is not this browser's).
import { composer as c } from "../../composer/composer.js";
const facts = (rows) => rows.filter(([, v]) => v).map(([k, v]) => c.settingsRow.default({ label: k, control: c.text.sm({ text: String(v) }) }));
export const about = (api) => function mount() {
const body = c.region.stack({ gap: "sm", label: "About" });
body.set([c.hint.default({ text: "Looking…" })]);
Promise.all([api.gitStatus().catch(() => ({})), api.system().catch(() => null)]).then(([git, sys]) => {
const major = String(git.version || "").split(".")[0];
const mem = (sys && sys.memory) || {}, cpu = (sys && sys.cpu) || {}, disk = (sys && sys.disk) || {}, torch = (sys && sys.torch) || {}, gpu = ((sys && sys.gpus) || [])[0];
body.set([
c.header.md({ text: `FunPack${major ? ` ${major}` : ""}` }),
c.hint.default({ text: [git.codename && `“${git.codename}”`, "Cutting Room"].filter(Boolean).join(" · ") }),
...facts([["Version", git.version], ["Commit", git.commit && String(git.commit).slice(0, 7)], ["Branch", git.branch]]),
...(sys ? [c.label.section({ text: "Hardware" }),
...facts([["Chip", cpu.name], ["Memory", mem.total_gb != null && `${mem.available_gb} GB free of ${mem.total_gb} GB`],
["Graphics", gpu ? [gpu.name, gpu.vram_gb && `${gpu.vram_gb} GB`].filter(Boolean).join(" · ") : sys.mps ? "Apple GPU (MPS)" : "CPU only"],
["Storage", disk.total_gb != null && `${disk.free_gb} GB available of ${disk.total_gb} GB`]]),
c.label.section({ text: "Software" }),
...facts([["System", sys.os], ["ComfyUI", sys.comfyui], ["Python", sys.python], ["Torch", [torch.version, torch.cuda && `CUDA ${torch.cuda}`].filter(Boolean).join(" · ")], ["Host", sys.host]]),
c.hint.default({ text: "The machine ComfyUI runs on — not this browser." })] : [c.hint.default({ text: "Machine details are not available from this server." })]),
]);
});
return body;
};
+8
View File
@@ -0,0 +1,8 @@
// Appearance: dark, light, or follow the system.
import { composer as c } from "../../composer/composer.js";
export const appearance = (theme) => function mount() {
return c.settingsRow.default({ label: "Colour scheme", hint: "Light, dark, or follow the system.",
control: c.segmented.sm({ label: "Colour scheme", value: theme.get(), onChange: (v) => theme.apply(v),
options: [{ value: "dark", label: "Dark" }, { value: "light", label: "Light" }, { value: "auto", label: "Auto" }] }) });
};
+33
View File
@@ -0,0 +1,33 @@
// Settings sections that sit in v4's window: the real ones, and stand-ins (named, grouped, saying what is coming) for the rest.
import { composer as c } from "../../composer/composer.js";
import { about } from "./about.js";
import { appearance } from "./appearance.js";
const later = (what) => function mount() {
return c.emptyState.default({ icon: "◌", title: "Not built yet", hint: what });
};
export default {
id: "settings-sections",
mount: "settings",
needs: ["api", "theme"],
setup({ host, app }) {
const add = (id, title, subtitle, icon, tone, mount, group = "", keywords = "") => host.add({ id, group, title, subtitle, icon, tone, mount, keywords: `${keywords} ${title}` });
add("about", "About FunPack", "", "◎", "accent", about(app.api), "", "version commit branch cpu memory gpu disk python torch");
add("appearance", "Appearance", "Light, dark, or follow the system.", "◐", "neutral", appearance(app.theme), "", "theme colour light dark auto");
add("editor", "Editor", "How the editor behaves. The open project remembers these, so they follow it to another machine.", "☰", "neutral",
later("Upscale model, prompt autocomplete, shortcut ideas, i2v options."), "", "upscale autocomplete shortcuts ideas preferences");
add("engine", "Engine", "Settings every installed module has volunteered, grouped by what they're for.", "⚡", "warn",
later("Continuity, guidance, conditioning, sampling and post settings of the installed modules."), "Generation", "engine module settings");
add("models", "Models & Pipeline", "Loaders and nodes wired into the live pipeline.", "⬡", "accent",
later("Pick the model, VAE, CLIP and LoRA files; see the live pipeline."), "Generation", "models loaders unet vae clip lora");
add("modules", "Modules", "Switch a module off for this project; see and re-enable ones that failed.", "☷", "neutral",
later("Per-module on/off for this project, and the ones that failed to load."), "Generation", "modules enable disable quarantine");
add("refinement", "Refinement & Taste", "Learned-taste state: refinement keys and the Absolute global-taste store.", "✦", "danger",
later("Refinement keys: export, delete; the taste store."), "Learning", "refinement taste keys rating");
add("system", "Updates & ComfyUI", "Server connection, FunPack code updates, pipeline health.", "⟳", "good",
later("Update, switch branch, restart ComfyUI, roll back."), "System", "update git branch restart rollback");
add("customnodes", "Custom Nodes", "Install, update and remove ComfyUI node packs.", "⧉", "neutral",
later("Install, update and remove node packs."), "System", "custom nodes packs install");
},
};
+1 -1
View File
@@ -4,7 +4,7 @@ export default {
id: "temp",
mount: "settings",
setup: ({ host }) => host.add({
id: "temp", title: "Temp files", subtitle: "Where a file went when it did not land in the bin.",
id: "temp", group: "System", title: "Temp Files", subtitle: "Where a file went when it did not land in the bin.",
keywords: "temp files output directory", icon: "▥", mount,
}),
};
+1
View File
@@ -12,5 +12,6 @@ export default [
"./features/render/index.js",
"./features/placeholders/index.js",
"./features/generate/warn.js",
"./features/settings/index.js",
"./features/temp/index.js",
].map((path) => new URL(path, import.meta.url).href);
+2
View File
@@ -22,6 +22,8 @@ export const api = {
pipeline: () => call("GET", "/api/pipeline"),
editPipeline: (body) => call("POST", "/api/pipeline", body),
modules: (traits) => call("GET", `/api/modules${traits ? `?traits=${encodeURIComponent(traits)}` : ""}`),
system: () => call("GET", "/api/system"),
gitStatus: () => call("GET", "/api/git/status?remote=0"),
media: () => call("GET", "/api/media"),
deleteMedia: (id) => call("DELETE", `/api/media/${encodeURIComponent(id)}`),
/** Upload files as media, one request each so one refusal (too big, wrong type) does not sink the rest.
+4 -3
View File
@@ -1,13 +1,14 @@
// The Settings window. It owns no section: features announce theirs by mounting at "settings"
// setup({ host }) { host.add({ id, title, subtitle, keywords, icon, tone, mount({ setFooter, close }) -> handle }) }
// setup({ host }) { host.add({ id, group?, title, subtitle, keywords, icon, tone, mount({ setFooter, close }) -> handle }) }
// and the window lists whatever arrived. It is also the one place a "Settings" button is made.
import { composer } from "../composer/composer.js";
import { offer } from "./mounts.js";
export function createSettings({ menubar }) {
const GROUPS = ["", "Generation", "Learning", "System"]; // the order v4 laid the window out in
const sections = [];
// childNodes: the feature loader checks a region's children to undo a feature that fails half-way
const host = { childNodes: [], add: (section) => sections.push(section) };
const host = { childNodes: [], add: (section) => { sections.push(section); sections.sort((a, b) => GROUPS.indexOf(a.group || "") - GROUPS.indexOf(b.group || "")); } };
offer("settings", host);
let current = null;
@@ -29,7 +30,7 @@ export function createSettings({ menubar }) {
}
const nav = composer.filterList.md({
items: sections.map((s) => ({ id: s.id, label: s.title, icon: s.icon, keywords: s.keywords, tone: s.tone || "neutral" })),
items: sections.map((s) => ({ id: s.id, label: s.title, icon: s.icon, keywords: s.keywords, tone: s.tone || "neutral", group: s.group || "" })),
value: id, onChange: show, placeholder: "Search settings",
});
nav.node.classList.add("cx-filter-full", "cx-settings-rail");