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.
This commit is contained in:
digital-garbage
2026-09-07 01:22:35 +03:00
parent e6e4dfa882
commit 563b1c6f02
6 changed files with 107 additions and 15 deletions
+5 -5
View File
@@ -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({
+36 -3
View File
@@ -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); }
+14 -3
View File
@@ -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); });
+11
View File
@@ -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; }
@@ -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");
+18 -4
View File
@@ -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 || "?")]);