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:
+5
-5
@@ -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({
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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); });
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 || "?")]);
|
||||
|
||||
Reference in New Issue
Block a user