// 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; }, }; }