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