An outside click on the Settings window closed it unconditionally -- pipeline_window.js's own standalone modal has always refused to close that way (a group edit's draft is the only copy of it), and hosting it inside Settings silently dropped that guarantee, with no way left for a real user to reach the protected version (boot.js has no other caller of pipeline_window.js any more). Settings now holds the door the same way, for every section: an accidental backdrop click during any half-finished edit no longer costs it. filterList's icon `tone` claimed to be a closed set, the same rule el()'s style refusal already holds -- but nothing checked it, so a typo would have rendered a silent, unstyled chip instead of failing loudly the way button.js's own tone check already does for the same mistake. Now it throws, matching that sibling pattern.
144 lines
5.9 KiB
JavaScript
144 lines
5.9 KiB
JavaScript
// One home for everything that is a PREFERENCE about the app or the pipeline,
|
|
// rather than an edit to the project on screen -- replacing five separate
|
|
// menu items (Models and pipeline, Updates, Node packs, Log, Temp files) that
|
|
// nothing tied together and nothing let you search.
|
|
//
|
|
// Every section is MOUNTED here, in place, beside the same always-visible
|
|
// nav list -- picking "Node packs" does not leave this window any more than
|
|
// clicking a different System Settings pane leaves System Settings. Each of
|
|
// pipeline_window.js/updates.js/packs.js/logwindow.js/tempfiles.js already
|
|
// had its content built separately from its own modal chrome (a `body`
|
|
// region.stack, footer buttons set on the modal handle); their `mount()`
|
|
// exports are that same content, taking `setFooter`/`close` from whoever is
|
|
// hosting them instead of owning a modal directly. Their `open()` exports
|
|
// are unchanged for anyone opening one standalone.
|
|
|
|
import { composer } from "../composer/composer.js";
|
|
|
|
/**
|
|
* createSettingsWindow({ sections, gitStatus }) -> { open, close }
|
|
*
|
|
* `sections`: [{ id, title, subtitle, keywords, icon,
|
|
* mount({ setFooter, close }) -> handle }].
|
|
* `mount()` returns a composer element handle (its own `destroy()` is the
|
|
* cleanup, called when another section is picked or the window closes).
|
|
* `setFooter`/`close` act on this window's own modal chrome -- a section
|
|
* does not know or care whether it is standing alone or hosted here.
|
|
*
|
|
* `gitStatus` is injected, not imported, for the same reason every other
|
|
* shell window takes its network calls as props: testable without a server,
|
|
* and unable to reach for one that was not handed to it.
|
|
*
|
|
* `current` lives HERE, per call, not at module scope: a module-level
|
|
* singleton would let a second independent settings window (there is only
|
|
* ever one in this app today, but nothing enforced that) silently redraw
|
|
* the FIRST one's modal instead of opening its own.
|
|
*/
|
|
export function createSettingsWindow({ sections = [], gitStatus } = {}) {
|
|
const all = [aboutSection(gitStatus), ...sections];
|
|
let current = null;
|
|
|
|
function open(id) {
|
|
if (current) { current.show(id); return current; }
|
|
|
|
let activeCleanup = null;
|
|
const contentStack = composer.region.stack({ gap: "sm", fill: true });
|
|
|
|
function showSection(pick) {
|
|
const spec = all.find((s) => s.id === pick) || all[0];
|
|
if (!spec) return;
|
|
if (activeCleanup) { activeCleanup.destroy?.(); activeCleanup = null; }
|
|
// Cleared before mounting: a section that sets no footer of its own
|
|
// (About) must not go on showing the PREVIOUS section's buttons.
|
|
modal.setFooter({});
|
|
const content = spec.mount({
|
|
setFooter: (f) => modal.setFooter(f),
|
|
close: () => modal.close("done"),
|
|
});
|
|
activeCleanup = content;
|
|
contentStack.set([
|
|
composer.header.md({ text: spec.title }),
|
|
spec.subtitle ? composer.hint.default({ text: spec.subtitle }) : null,
|
|
content,
|
|
]);
|
|
nav.setValue(spec.id);
|
|
}
|
|
|
|
const nav = composer.filterList.md({
|
|
// No hint line here -- an icon and a name, the way v4's own sidebar
|
|
// reads. What a section IS goes in its own head once picked, not
|
|
// repeated under every row whether picked or not.
|
|
items: all.map((s) => ({
|
|
id: s.id, label: s.title, icon: s.icon, keywords: s.keywords,
|
|
tone: s.tone || "neutral",
|
|
})),
|
|
value: id,
|
|
onChange: showSection,
|
|
placeholder: "Search settings",
|
|
});
|
|
// A whole pane, not the compact dropdown-style list this element is
|
|
// usually a small part of.
|
|
nav.node.classList.add("cx-filter-full");
|
|
|
|
const body = composer.splitPane.h({ panes: [nav, contentStack], size: 25, min: 15, label: "Settings" });
|
|
const modal = composer.modal.generic({
|
|
title: "Settings", size: "xl", body,
|
|
// Mirrors pipeline_window.js's own standalone modal: a half-finished
|
|
// edit (Models and pipeline's draft, most concretely) must not vanish
|
|
// because a click landed on the backdrop. This window has no way to
|
|
// ask "does the section on screen have unsaved work" per section, so
|
|
// it holds the door for all of them, the same conservative call
|
|
// pipeline_window.js already made for itself -- the X button and every
|
|
// section's own "Close" still work.
|
|
closeOnOutside: false,
|
|
onClose: () => {
|
|
if (activeCleanup) { activeCleanup.destroy?.(); activeCleanup = null; }
|
|
if (current === handle) current = null;
|
|
},
|
|
});
|
|
|
|
const handle = { node: modal.node, close: () => modal.close(), show: showSection };
|
|
current = handle;
|
|
showSection(id || (all[0] && all[0].id));
|
|
return handle;
|
|
}
|
|
|
|
function close() {
|
|
if (current) current.close();
|
|
}
|
|
|
|
return { open, close };
|
|
}
|
|
|
|
/** Version and branch -- the one thing everyone with the window open at all
|
|
* might be there to check, so it is built in rather than one more click away. */
|
|
function aboutSection(gitStatus) {
|
|
return {
|
|
id: "about", title: "About FunPack",
|
|
subtitle: "The version and branch running right now.",
|
|
keywords: "about version commit branch",
|
|
icon: "◉", tone: "neutral",
|
|
mount() {
|
|
const stack = composer.region.stack({ gap: "md" });
|
|
stack.node.classList.add("cx-about");
|
|
const rows = composer.region.stack({ gap: "none" });
|
|
const row = (label, value) => composer.settingsRow.default({
|
|
label, control: composer.text.sm({ text: value }) });
|
|
rows.set([row("Version", "…"), row("Branch", "…")]);
|
|
stack.set([
|
|
composer.text.lg({ text: "◉" }),
|
|
composer.header.lg({ text: "FunPack" }),
|
|
rows,
|
|
]);
|
|
if (gitStatus) {
|
|
gitStatus().then((s) => {
|
|
rows.set([row("Version", s.version || "unknown"), row("Branch", s.branch || "?")]);
|
|
}).catch(() => {
|
|
rows.set([composer.hint.default({ text: "Could not read version information." })]);
|
|
});
|
|
}
|
|
return stack;
|
|
},
|
|
};
|
|
}
|