From 563b1c6f02611ec2f5897a0f7fbf036c14189aaa Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:22:35 +0300 Subject: [PATCH] app: Settings looks designed instead of default -- icon chips, real sidebar, centred About A bare glyph character had no framing at all; filterList's icon now gets a coloured chip, drawn from a fixed tone (accent/danger/warn/good/neutral) -- never a raw colour from data, the same rule el()'s style refusal already holds everywhere else. Each Settings section picks one. The sidebar was the filterList element's small dropdown-list shape (a 240px-capped box with its own border, floating in a large pane) rather than a real full-height sidebar; .cx-filter-full opts a host into the pane version without changing the compact one pipeline_window's own node picker still uses. Round one had a per-row hint line under the icon+label, which doesn't exist in v4's own sidebar and broke alignment the moment a hint wrapped (icon and label baseline-aligned against a hint floating as an independent sibling rather than stacked under its own label). Fixed the real bug -- label+hint are a column now, not loose flex siblings -- then removed the hint entirely to match v4: a section's subtitle belongs in its own head once picked, not repeated under every row whether picked or not. About is centred -- mark, name, version/branch -- rather than a plain settings-row list, the one section built fresh rather than mounting an existing window. --- app/boot.js | 10 ++--- app/composer/elements/input.css | 39 +++++++++++++++++-- app/composer/elements/input.js | 17 ++++++-- app/composer/elements/layout.css | 11 ++++++ app/composer/tests/elements_behaviour.test.js | 23 +++++++++++ app/shell/settings_window.js | 22 +++++++++-- 6 files changed, 107 insertions(+), 15 deletions(-) diff --git a/app/boot.js b/app/boot.js index 225a1f33..3a66c14f 100644 --- a/app/boot.js +++ b/app/boot.js @@ -201,7 +201,7 @@ async function start() { sections: [ { id: "pipeline", title: "Models and pipeline", subtitle: "What the run is made of.", - keywords: "pipeline models nodes slots loaders", icon: "▦", + keywords: "pipeline models nodes slots loaders", icon: "▦", tone: "accent", mount: (ctx) => mountPipeline({ load, describe, check, search, ...ctx, onApply: (next) => { @@ -221,7 +221,7 @@ async function start() { }, { id: "updates", title: "Updates", subtitle: "FunPack's own code, branch, and restart.", - keywords: "update git branch restart pull checkout", icon: "⟳", + keywords: "update git branch restart pull checkout", icon: "⟳", tone: "warn", // The window asks whether a run is in flight, because the restart that // follows an update would take it with it. mount: (ctx) => mountUpdates({ @@ -230,17 +230,17 @@ async function start() { }, { id: "packs", title: "Node packs", subtitle: "Install, update, or remove a custom_nodes pack.", - keywords: "node packs custom_nodes install", icon: "▣", + keywords: "node packs custom_nodes install", icon: "▣", tone: "good", mount: (ctx) => mountPacks(ctx), }, { id: "log", title: "ComfyUI log", subtitle: "The server's own log, without opening devtools.", - keywords: "log console errors output", icon: "▤", + keywords: "log console errors output", icon: "▤", tone: "danger", mount: (ctx) => mountLog(ctx), }, { id: "temp", title: "Temp files", subtitle: "Where a file went when it did not land in the bin.", - keywords: "temp files output directory", icon: "▥", + keywords: "temp files output directory", icon: "▥", tone: "neutral", // Opening one puts it in the Preview, which is where somebody hunting // for a file wants it -- the same place a result from the bin goes. mount: (ctx) => mountTemp({ diff --git a/app/composer/elements/input.css b/app/composer/elements/input.css index 36b8ae7e..231b9c17 100644 --- a/app/composer/elements/input.css +++ b/app/composer/elements/input.css @@ -43,13 +43,46 @@ background: var(--elev-1); padding: var(--sp-1); } .cx-filter-row { - display: flex; align-items: baseline; gap: var(--sp-3); + display: flex; align-items: flex-start; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border: 0; border-radius: var(--r-2); background: transparent; color: var(--text); font-size: var(--fs-5); text-align: left; cursor: pointer; width: 100%; } .cx-filter-row:hover { background: var(--hover); } .cx-filter-row.cx-on { background: var(--accent-soft); color: var(--accent-hi); } -.cx-filter-label { flex: 1; } -.cx-filter-hint { font-size: var(--fs-7); color: var(--muted); } +/* A column so a wrapping hint sits directly under its own label, both flush + to the same left edge, instead of floating as a baseline-aligned sibling + of the icon and the label. */ +.cx-filter-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } +.cx-filter-label { display: block; } +.cx-filter-hint { display: block; font-size: var(--fs-7); color: var(--muted); line-height: var(--lh-snug); } .cx-filter-empty { padding: var(--sp-5); font-size: var(--fs-6); color: var(--faint); text-align: center; } + +/* A chip, not a bare glyph floating in the row -- the thing a nav list reads + as unfinished without. `tone` is a closed set of pre-approved looks + (accent/danger/warn/good/neutral), the same rule as everywhere else in the + kit; there is no path from item data to a raw colour. */ +.cx-filter-icon { + display: inline-flex; align-items: center; justify-content: center; + width: 22px; height: 22px; flex: 0 0 auto; border-radius: var(--r-2); + background: var(--elev-3); color: var(--text); font-size: var(--fs-6); line-height: 1; + /* Optical, against the label's cap-height, not the row's own top edge. */ + margin-block-start: 1px; +} +.cx-filter-icon-accent { background: var(--grad-accent); color: var(--on-accent); } +.cx-filter-icon-danger { background: linear-gradient(180deg, var(--danger), color-mix(in srgb, var(--danger) 65%, black)); color: #fff; } +.cx-filter-icon-warn { background: linear-gradient(180deg, var(--warn), color-mix(in srgb, var(--warn) 65%, black)); color: #fff; } +.cx-filter-icon-good { background: linear-gradient(180deg, var(--good), color-mix(in srgb, var(--good) 65%, black)); color: #fff; } +.cx-filter-icon-neutral { background: linear-gradient(180deg, var(--muted), color-mix(in srgb, var(--muted) 65%, black)); color: #fff; } +.cx-filter-row.cx-on .cx-filter-icon { box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-hi) 50%, transparent); } + +/* Used as a whole pane (the Settings sidebar) rather than a compact dropdown + list -- the 240px cap and boxed border/background are right for the small + version and wrong for this one, so a host opts in with this class instead + of the base rules changing under every other consumer. */ +.cx-filter-full { min-block-size: 0; block-size: 100%; } +.cx-filter-full .cx-filter-list { + max-height: none; flex: 1 1 auto; min-block-size: 0; + border: 0; background: transparent; padding: 0; +} +.cx-filter-full .cx-filter-row { padding: var(--sp-3) var(--sp-3); } diff --git a/app/composer/elements/input.js b/app/composer/elements/input.js index 276198dd..f6692cb5 100644 --- a/app/composer/elements/input.js +++ b/app/composer/elements/input.js @@ -172,9 +172,20 @@ define("filterList", "md", ({ items = [], value, onChange, placeholder = "Search cls: ["cx-filter-row", "cx-focusable", item.id === selected ? "cx-on" : null], attrs: { type: "button", role: "option", "aria-selected": String(item.id === selected) }, children: [ - item.icon ? el("span", { cls: "cx-filter-icon", text: item.icon }) : null, - el("span", { cls: "cx-filter-label", text: item.label }), - item.hint ? el("span", { cls: "cx-filter-hint", text: item.hint }) : null, + // `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. + item.icon ? el("span", { + cls: ["cx-filter-icon", item.tone ? `cx-filter-icon-${item.tone}` : null], + text: item.icon, + }) : null, + // A column, not two more flex siblings beside the icon -- a hint + // long enough to wrap needs to wrap UNDER its own label, not float + // as its own baseline-aligned sibling next to it. + el("div", { cls: "cx-filter-text", children: [ + el("span", { cls: "cx-filter-label", text: item.label }), + item.hint ? el("span", { cls: "cx-filter-hint", text: item.hint }) : null, + ].filter(Boolean) }), ].filter(Boolean), }); row.addEventListener("click", () => { selected = item.id; draw(); if (onChange) onChange(item.id); }); diff --git a/app/composer/elements/layout.css b/app/composer/elements/layout.css index 1699156c..72e72323 100644 --- a/app/composer/elements/layout.css +++ b/app/composer/elements/layout.css @@ -403,3 +403,14 @@ /* A split inside a stack is what stretches. The rows around it -- a toolbar, a banner, an action bar -- are each their own height. */ .cx-stack > .cx-split { flex: 1 1 auto; min-block-size: 0; } + +/* A stack that is a small poster rather than a form -- an "About" pane's + mark, name and facts, centred instead of ranged left like every settings + list around it. */ +.cx-about { align-items: center; text-align: center; padding-block: var(--sp-7) 0; } +.cx-about > .cx-t-lg { font-size: 2.5rem; line-height: 1; color: var(--accent); } +.cx-about .cx-settings-row { + justify-content: center; gap: var(--sp-4); border-bottom: 0; padding-block: var(--sp-2); + inline-size: auto; align-self: center; +} +.cx-about .cx-settings-control { flex: 0 0 auto; justify-content: flex-start; } diff --git a/app/composer/tests/elements_behaviour.test.js b/app/composer/tests/elements_behaviour.test.js index e0fd9ac2..89e1d99e 100644 --- a/app/composer/tests/elements_behaviour.test.js +++ b/app/composer/tests/elements_behaviour.test.js @@ -215,6 +215,29 @@ test("filterList filters on label and hint, and reports a pick", () => { assert.equal(picked, "a"); }); +test("filterList filters on keywords too, though it never draws them", () => { + const f = mount(composer.filterList.md({ + items: [ + { id: "a", label: "Models and pipeline", hint: "What the run is made of.", keywords: "loaders" }, + { id: "b", label: "Updates" }, + ], + })); + const search = f.node.querySelector("input"); + search.value = "loaders"; + fire(search, "input"); + const rows = f.node.querySelectorAll(".cx-filter-row"); + assert.equal(rows.length, 1, "a section findable only by a keyword must still be found"); + assert.equal(f.node.textContent.includes("loaders"), false, "keywords are for search, not display"); +}); + +test("filterList gives an icon a coloured chip only from a fixed tone, never from raw item data", () => { + const f = mount(composer.filterList.md({ + items: [{ id: "a", label: "Node packs", icon: "▣", tone: "good" }], + })); + const icon = f.node.querySelector(".cx-filter-icon"); + assert.ok(icon.classList.contains("cx-filter-icon-good")); +}); + test("filterList says so when nothing matches", () => { const f = mount(composer.filterList.md({ items: [{ id: "a", label: "one" }] })); const search = f.node.querySelector("input"); diff --git a/app/shell/settings_window.js b/app/shell/settings_window.js index 07b01e1a..38111519 100644 --- a/app/shell/settings_window.js +++ b/app/shell/settings_window.js @@ -65,11 +65,20 @@ export function createSettingsWindow({ sections = [], gitStatus } = {}) { } const nav = composer.filterList.md({ - items: all.map((s) => ({ id: s.id, label: s.title, hint: s.subtitle, icon: s.icon, keywords: s.keywords })), + // 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({ @@ -100,14 +109,19 @@ function aboutSection(gitStatus) { id: "about", title: "About FunPack", subtitle: "The version and branch running right now.", keywords: "about version commit branch", - icon: "◉", + icon: "◉", tone: "neutral", mount() { - const stack = composer.region.stack({ gap: "sm" }); + 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([rows]); + 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 || "?")]);