diff --git a/CHANGELOG.md b/CHANGELOG.md
index a239a34..cc12175 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,13 @@ The format is inspired by Keep a Changelog and SemVer.
no-context-menu section header). Fresh-install path is unchanged — the
Save modal's "+ New directory…" entry still creates the first directory
when the workflow store is empty. Closes #104.
+- **Kforge Labs sidebar action icons now use a first-pass ForgeFlow-style
+ icon set.** Snapshot Load / Save, Tools export / install / help, the
+ missing-pack canvas handoff, repo-mode grouping, and whole-canvas workflow
+ save now use the approved compact letter / simple-glyph treatment from
+ `docs/designs/sidebar-icon-proposals.html`, reducing repeated generic
+ cloud / grid / floppy symbols while leaving all actions and tooltips
+ unchanged. Tools now also exposes an `H` help shortcut to the user guide.
- **Node-list hover previews now stay at the intended 300px mock-node width.**
The preview card previously had only a `min-width` plus viewport `max-width`,
so long node descriptions could set the card's intrinsic width and stretch the
diff --git a/docs/README.md b/docs/README.md
index 823d0fb..0cadf49 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,6 +7,7 @@ Three audience buckets — pick where to look based on what you're trying to do.
| Use a node in your ComfyUI workflow | [`user_guide/`](user_guide/) | Per-node guides, screenshots, recipes |
| Look something up — a term, a node ID, a list | [`reference/`](reference/) | [Glossary](reference/glossary.md), node inventory, spec sheets |
| Cut a release / fork a node / debug CI / hit the Comfy Registry API | [`maintainers/`](maintainers/) | [Releasing](maintainers/releasing.md), [Registry API](maintainers/registry-api.md), [Node versioning rules](maintainers/node-versioning.md) |
+| Compare UI design proposals before implementation | [`designs/`](designs/) | Focused mockups and screenshots for in-progress UX decisions |
## Files that intentionally stay at the repo root
diff --git a/docs/designs/README.md b/docs/designs/README.md
new file mode 100644
index 0000000..fcea2af
--- /dev/null
+++ b/docs/designs/README.md
@@ -0,0 +1,9 @@
+# Design Proposals
+
+Small, reviewable UI mockups for decisions that benefit from visual comparison
+before they land in the runtime sidebar.
+
+| File | Purpose |
+|---|---|
+| [`sidebar-icon-proposals.html`](sidebar-icon-proposals.html) | Side-by-side icon direction mockup for the Kforge Labs sidebar action rows |
+| [`sidebar-icon-proposals.png`](sidebar-icon-proposals.png) | Rendered preview of the current icon proposal state |
diff --git a/docs/designs/sidebar-icon-proposals.html b/docs/designs/sidebar-icon-proposals.html
new file mode 100644
index 0000000..afc89ad
--- /dev/null
+++ b/docs/designs/sidebar-icon-proposals.html
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+ Kforge Labs Sidebar Icon Proposals
+
+
+
+
+ Kforge Labs Sidebar Icon Proposals
+ Two visual directions for replacing the generic action icons. Both preserve the current compact ComfyUI sidebar density; only the icon language and semantic color treatment change.
+
+
+
+
+
Proposal A — ForgeFlow Polish
+
Conservative: refined line icons, clearer semantic color, familiar shapes. Lowest implementation risk.
+
+
+ Best when we want an immediate improvement without inventing a branded icon system. The main change is semantic consistency: sky means mode/selection, green means primary add/create, neutral means utility.
+
+
+
+
+
Proposal B — Kforge Glyphs
+
More custom: angular glyphs, paired “state capsule” buttons, and stronger Kforge/Fabrication flavor.
+
+
+ Best if we want a recognizable Kforge look. More distinctive, but it should ship as a small local icon registry so the shapes stay consistent over time.
+
+
+
+
+
diff --git a/docs/designs/sidebar-icon-proposals.png b/docs/designs/sidebar-icon-proposals.png
new file mode 100644
index 0000000..30fd7ae
Binary files /dev/null and b/docs/designs/sidebar-icon-proposals.png differ
diff --git a/web/sidebar/constants.js b/web/sidebar/constants.js
index 6cc9a0b..85caa33 100644
--- a/web/sidebar/constants.js
+++ b/web/sidebar/constants.js
@@ -105,7 +105,7 @@ export function ensureStyle() {
s.textContent = `
.koolook-sidebar { display: flex; flex-direction: column; height: 100%; font-size: 13px; user-select: none; padding-top: 10px; }
.koolook-search-row { margin: 6px; flex-shrink: 0; }
-.koolook-actions-row { display: flex; align-items: center; gap: 4px; padding: 2px 6px; flex-shrink: 0; }
+.koolook-actions-row { display: flex; align-items: center; gap: 3px; padding: 2px 6px; flex-shrink: 0; }
.koolook-actions-label { font-size: 10px; opacity: 0.55; text-transform: uppercase; letter-spacing: 0.08em; flex: 1; font-weight: 600; }
/* Snapshot status indicator — replaces the static "Snapshot" label. Shows
the currently-tracked preset name + a colored dot indicating whether the
@@ -126,8 +126,8 @@ export function ensureStyle() {
.koolook-search-icon { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); opacity: 0.55; font-size: 11px; pointer-events: none; }
.koolook-search { width: 100%; padding: 5px 8px 5px 26px; box-sizing: border-box; background: var(--comfy-input-bg, rgba(0,0,0,0.25)); border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 4px; color: var(--input-text, inherit); font-size: 12px; outline: none; }
.koolook-search:focus { border-color: var(--p-primary-color, rgba(100,150,255,0.5)); }
-.koolook-add-btn { padding: 0 10px; background: var(--comfy-input-bg, rgba(0,0,0,0.25)); border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 4px; cursor: pointer; color: var(--input-text, inherit); font-size: 12px; line-height: 1; flex-shrink: 0; }
-.koolook-add-btn:hover { background: rgba(255,255,255,0.1); }
+.koolook-add-btn { padding: 0; width: 28px; height: 17px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; background: var(--comfy-input-bg, rgba(0,0,0,0.25)); border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 3px; cursor: pointer; color: rgba(218,221,226,0.78); font-size: 12px; line-height: 1; flex-shrink: 0; }
+.koolook-add-btn:hover { background: rgba(255,255,255,0.1); color: rgba(248,249,250,0.96); }
.koolook-add-btn:active { background: rgba(255,255,255,0.15); }
/* Dusty-green accent on the "+" Add-to-favorites button — distinguishes the
primary action in the Nodes row from the neutral toolbar buttons it sits
@@ -135,8 +135,17 @@ export function ensureStyle() {
.koolook-add-btn-green { background: rgba(120, 165, 100, 0.25); border-color: rgba(120, 165, 100, 0.5); }
.koolook-add-btn-green:hover { background: rgba(120, 165, 100, 0.45); }
.koolook-add-btn-green:active { background: rgba(120, 165, 100, 0.6); }
-.koolook-icon-btn { padding: 0 10px; font-size: 12px; }
+.koolook-icon-btn { padding: 0; width: 28px; height: 17px; font-size: 12px; }
.koolook-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
+.koolook-icon-btn > .pi { font-size: 11px; }
+.koolook-letter-icon { font-size: 11px; font-weight: 800; line-height: 1; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
+.koolook-inline-svg-icon { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
+.koolook-filled-square-icon { display: inline-block; width: 10px; height: 10px; border-radius: 2px; background: currentColor; opacity: 0.9; }
+.koolook-stair-icon, .koolook-list-icon { display: inline-grid; grid-template-rows: repeat(3, 1.75px); gap: 2px; width: 15px; height: 10px; align-content: center; }
+.koolook-stair-icon span, .koolook-list-icon span { display: block; width: 11px; height: 1.75px; background: currentColor; border-radius: 0; }
+.koolook-stair-icon span:nth-child(2) { margin-left: 2px; }
+.koolook-stair-icon span:nth-child(3) { margin-left: 4px; }
+.koolook-add-btn-green > .pi { font-size: 13px; -webkit-text-stroke: 0.5px currentColor; }
.koolook-tree { flex: 1; overflow-y: auto; padding: 0 4px 8px; }
.koolook-row { display: flex; align-items: center; padding: 3px 6px; cursor: pointer; gap: 6px; border-radius: 3px; line-height: 1.3; }
.koolook-row:hover { background: var(--comfy-input-bg, rgba(255,255,255,0.06)); }
@@ -262,10 +271,10 @@ export function ensureStyle() {
.koolook-snapshot-row-btn:hover { background: rgba(255,255,255,0.08); }
.koolook-snapshot-row-btn-danger:hover { background: rgba(220,80,80,0.25); border-color: rgba(220,80,80,0.4); }
.koolook-snapshot-empty { padding: 20px; opacity: 0.5; font-size: 12px; text-align: center; }
-.koolook-mode-toggle { display: inline-flex; gap: 0; margin-right: 4px; border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 4px; overflow: hidden; }
-.koolook-mode-toggle-btn { padding: 0 8px; height: 22px; background: transparent; border: none; cursor: pointer; color: inherit; font-size: 11px; opacity: 0.5; line-height: 1; }
-.koolook-mode-toggle-btn:hover { background: rgba(255,255,255,0.06); opacity: 0.85; }
-.koolook-mode-toggle-btn.koolook-mode-active { background: rgba(80,140,235,0.25); opacity: 1; }
+.koolook-mode-toggle { display: inline-flex; gap: 0; margin-right: 3px; border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 3px; overflow: hidden; }
+.koolook-mode-toggle-btn { padding: 0; width: 28px; height: 17px; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; background: transparent; border: none; cursor: pointer; color: rgba(218,221,226,0.78); font-size: 11px; opacity: 1; line-height: 1; }
+.koolook-mode-toggle-btn:hover { background: rgba(255,255,255,0.06); color: rgba(248,249,250,0.96); }
+.koolook-mode-toggle-btn.koolook-mode-active { background: rgba(80,140,235,0.25); color: rgba(248,249,250,0.96); }
.koolook-leaf-unresolved { opacity: 0.55; font-style: italic; }
.koolook-leaf-crumb { opacity: 0.5; font-size: 11px; margin-right: 1px; }
.koolook-pack-badge { opacity: 0.5; font-size: 11px; margin-left: 6px; flex-shrink: 0; white-space: nowrap; }
diff --git a/web/sidebar/tree.js b/web/sidebar/tree.js
index b4779ae..b42a108 100644
--- a/web/sidebar/tree.js
+++ b/web/sidebar/tree.js
@@ -117,6 +117,38 @@ const SECTION_ID_NODES = "nodes";
const SECTION_ID_WORKFLOWS = "workflows";
const SECTION_ID_TAGS = "tags";
+const SVG_NS = "http://www.w3.org/2000/svg";
+const HELP_URL = "https://github.com/malkuthro/ComfyUI-Koolook/tree/main/docs/user_guide";
+
+const TOOLBAR_ICONS = {
+ loadSnapshot: { kind: "letter", text: "L" },
+ saveSnapshot: { kind: "letter", text: "S" },
+ help: { kind: "letter", text: "H" },
+ exportStarter: { kind: "letter", text: "E" },
+ installMissing: { kind: "letter", text: "I" },
+ dropMissing: {
+ kind: "svg",
+ shapes: [
+ ["path", { d: "M10 10 5 5" }],
+ ["path", { d: "M5 5v5" }],
+ ["path", { d: "M5 5h5" }],
+ ["path", { d: "m14 10 5-5" }],
+ ["path", { d: "M19 5v5" }],
+ ["path", { d: "M19 5h-5" }],
+ ["path", { d: "m10 14-5 5" }],
+ ["path", { d: "M5 19v-5" }],
+ ["path", { d: "M5 19h5" }],
+ ["path", { d: "m14 14 5 5" }],
+ ["path", { d: "M19 19v-5" }],
+ ["path", { d: "M19 19h-5" }],
+ ["circle", { cx: "12", cy: "12", r: "1.2" }],
+ ],
+ },
+ repoMode: { kind: "stair" },
+ categoryMode: { kind: "list" },
+ saveWorkflow: { kind: "square" },
+};
+
// =============================================================================
// Folder expansion state — Map; truthy = expanded.
// Path keys are auto-prefixed by the engine with the owning section's id, so
@@ -1160,26 +1192,66 @@ function gatherTags(query) {
// DOM helpers
// =============================================================================
-// Toolbar button factory — replaces the four near-identical button-construction
-// blocks in `renderPanel` for icon-only buttons (export, new-dir, save-canvas,
-// save-selection). The "+" Add-to-favorites button (`addBtn`) is text-content
-// only and stays hand-rolled; this factory does not cover that case. Every
-// covered button shares the `koolook-add-btn koolook-icon-btn` pair (both
-// styling rules; the disabled state on `:disabled` lives on `koolook-icon-btn`).
-// `iconClass` is the PrimeIcons class (e.g. `pi pi-download`).
-//
-// The icon span is built with `createElement` + `className` rather than
-// `innerHTML = …${iconClass}…` so a future caller passing a non-static
-// `iconClass` (e.g. data sourced from a workflow or registry) can't inject
-// markup. All current callers pass static literals — this is helper-boundary
-// hardening, not a fix for an active vulnerability.
-function makeToolbarButton({ iconClass, title, onClick }) {
+function makeIconElement(icon) {
+ if (icon && icon.kind === "letter") {
+ const el = document.createElement("span");
+ el.className = "koolook-letter-icon";
+ el.textContent = icon.text;
+ el.setAttribute("aria-hidden", "true");
+ return el;
+ }
+ if (icon && icon.kind === "square") {
+ const el = document.createElement("span");
+ el.className = "koolook-filled-square-icon";
+ el.setAttribute("aria-hidden", "true");
+ return el;
+ }
+ if (icon && icon.kind === "stair") {
+ const el = document.createElement("span");
+ el.className = "koolook-stair-icon";
+ el.setAttribute("aria-hidden", "true");
+ for (let i = 0; i < 3; i += 1) {
+ el.appendChild(document.createElement("span"));
+ }
+ return el;
+ }
+ if (icon && icon.kind === "list") {
+ const el = document.createElement("span");
+ el.className = "koolook-list-icon";
+ el.setAttribute("aria-hidden", "true");
+ for (let i = 0; i < 3; i += 1) {
+ el.appendChild(document.createElement("span"));
+ }
+ return el;
+ }
+ if (icon && icon.kind === "svg") {
+ const svg = document.createElementNS(SVG_NS, "svg");
+ svg.classList.add("koolook-inline-svg-icon");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ svg.setAttribute("aria-hidden", "true");
+ for (const [tag, attrs] of icon.shapes || []) {
+ const shape = document.createElementNS(SVG_NS, tag);
+ for (const [key, value] of Object.entries(attrs)) {
+ shape.setAttribute(key, value);
+ }
+ svg.appendChild(shape);
+ }
+ return svg;
+ }
+ const el = document.createElement("span");
+ el.className = typeof icon === "string" ? icon : (icon?.iconClass || "");
+ el.setAttribute("aria-hidden", "true");
+ return el;
+}
+
+// Toolbar button factory for icon-only action buttons. Icons are passed as
+// either PrimeIcon class strings or static local glyph descriptors.
+function makeToolbarButton({ iconClass, icon, title, onClick }) {
const btn = document.createElement("button");
btn.className = "koolook-add-btn koolook-icon-btn";
- const icon = document.createElement("span");
- icon.className = iconClass;
- btn.appendChild(icon);
+ btn.appendChild(makeIconElement(icon || iconClass));
btn.title = title;
+ btn.setAttribute("aria-label", title);
btn.addEventListener("click", onClick);
return btn;
}
@@ -2255,7 +2327,7 @@ export function renderPanel(container) {
window.addEventListener(SNAPSHOT_STATUS_CHANGED_EVENT, refreshSnapshotStatus);
snapshotRow.appendChild(makeToolbarButton({
- iconClass: "pi pi-cloud-download",
+ icon: TOOLBAR_ICONS.loadSnapshot,
title: "Load a saved snapshot (replaces current state)",
onClick: openLoadDialog,
}));
@@ -2291,7 +2363,7 @@ export function renderPanel(container) {
window.addEventListener(SNAPSHOT_STATUS_CHANGED_EVENT, refreshQuickSaveDisabled);
snapshotRow.appendChild(quickSaveBtn);
snapshotRow.appendChild(makeToolbarButton({
- iconClass: "pi pi-cloud-upload",
+ icon: TOOLBAR_ICONS.saveSnapshot,
title: "Save current state — overwrites the last-loaded preset, or prompts for a name",
onClick: openSaveDialog,
}));
@@ -2325,7 +2397,7 @@ export function renderPanel(container) {
toolsRow.appendChild(toolsLabel);
toolsRow.appendChild(makeToolbarButton({
- iconClass: "pi pi-download",
+ icon: TOOLBAR_ICONS.exportStarter,
title: "Export current state as starter_preset.json (copies snapshot JSON to clipboard — paste into web/starter_preset.json to ship as the next release's starter)",
onClick: exportStarterPreset,
}));
@@ -2338,7 +2410,7 @@ export function renderPanel(container) {
// confirmation. Click handlers don't read picks here — the modal pulls
// a fresh `loadUserPicks()` snapshot at open-time.
toolsRow.appendChild(makeToolbarButton({
- iconClass: "pi pi-cloud-download",
+ icon: TOOLBAR_ICONS.installMissing,
title: "Install missing custom nodes for current picks (via ComfyUI-Manager)",
onClick: () => showInstallMissingModal({ picks: loadUserPicks() }),
}));
@@ -2352,7 +2424,7 @@ export function renderPanel(container) {
// handoff to Manager's UI-driven install path that doesn't go through
// /customnode/install/git_url's security gate.
toolsRow.appendChild(makeToolbarButton({
- iconClass: "pi pi-th-large",
+ icon: TOOLBAR_ICONS.dropMissing,
title: "Drop placeholders for missing packs onto canvas — then use Manager's \"Install Missing Custom Nodes\" (works at security_level=normal)",
onClick: async () => {
const picks = loadUserPicks();
@@ -2378,6 +2450,12 @@ export function renderPanel(container) {
},
}));
+ toolsRow.appendChild(makeToolbarButton({
+ icon: TOOLBAR_ICONS.help,
+ title: "Open Kforge Labs user guide",
+ onClick: () => window.open(HELP_URL, "_blank", "noopener,noreferrer"),
+ }));
+
container.appendChild(toolsRow);
const dividerBeforeSearch = document.createElement("div");
@@ -2427,14 +2505,13 @@ export function renderPanel(container) {
const modeToggle = document.createElement("div");
modeToggle.className = "koolook-mode-toggle";
- function makeModeBtn(modeId, iconClass, title) {
+ function makeModeBtn(modeId, icon, title) {
const btn = document.createElement("button");
btn.className = "koolook-mode-toggle-btn";
btn.title = title;
+ btn.setAttribute("aria-label", title);
btn.dataset.mode = modeId;
- const icon = document.createElement("span");
- icon.className = iconClass;
- btn.appendChild(icon);
+ btn.appendChild(makeIconElement(icon));
btn.addEventListener("click", () => {
if (loadGroupMode() === modeId) return;
saveGroupMode(modeId);
@@ -2446,12 +2523,12 @@ export function renderPanel(container) {
const repoModeBtn = makeModeBtn(
"repo",
- "pi pi-sitemap",
+ TOOLBAR_ICONS.repoMode,
"Group by pack — each repo's nodes under their original category subtree.",
);
const categoryModeBtn = makeModeBtn(
"category",
- "pi pi-bars",
+ TOOLBAR_ICONS.categoryMode,
"Group by theme — picks regrouped by category theme (e.g. all image nodes together) regardless of source pack.",
);
@@ -2474,8 +2551,10 @@ export function renderPanel(container) {
addBtn.className = "koolook-add-btn koolook-icon-btn koolook-add-btn-green";
const addBtnIcon = document.createElement("i");
addBtnIcon.className = "pi pi-plus";
+ addBtnIcon.setAttribute("aria-hidden", "true");
addBtn.appendChild(addBtnIcon);
addBtn.title = "Add the selected canvas node(s) to favorites";
+ addBtn.setAttribute("aria-label", "Add the selected canvas node(s) to favorites");
addBtn.addEventListener("click", () => {
const types = getSelectedNodeTypes();
if (types.length === 0) {
@@ -2582,7 +2661,7 @@ export function renderPanel(container) {
};
wfRow.appendChild(makeToolbarButton({
- iconClass: "pi pi-save",
+ icon: TOOLBAR_ICONS.saveWorkflow,
title: "Save entire canvas as a workflow",
onClick: () => {
// Check emptiness first so a serialize() throw doesn't get