feat(ui): add Focused Prompt Manager with shared autocomplete (#191)

* feat(ui): add Focused Prompt Manager node (#186)

* feat(ui): autocomplete saved Focused metadata with shared controls (#186)

* feat(ui): autocomplete Focused library filters (#186)
This commit is contained in:
Vito
2026-10-17 05:31:24 -07:00
committed by GitHub
parent 035e816db6
commit a52ff4e052
40 changed files with 1484 additions and 57 deletions
+11
View File
@@ -21,3 +21,14 @@ range, touch-target and forced-colors rules (original lines 1–61, 97–99, 118
170–172 and closing brace 174). Feature-view selectors remain in KikoTools.
The retained declarations are unchanged; the extracted stylesheet SHA-256 is
`3ae1ca39077ffc3621b86ae7ef77202841fa27061b0e4623f2bb0413b27ecd7b`.
## Suggestion view extraction
`ui/suggestions.js` derives from KikoTools `web/ui/autocomplete-suggestions.js`
at commit `1820fb19ea5d200e8cd2a09bcbbcc35926f13228` (SHA-256 `2aecc78f53b7be3d67b77337b4a340f948afcc4cb1196ccf3b70739ef1f0d002`).
The generic view preserves its scope-based cleanup, safe text rendering and
viewport positioning. Pack-specific classes and IDs are replaced by `ca-` names;
callers can supply a list label and omit type badges. Corresponding completion
styles are extracted from KikoTools' `web/ui/controls.css`. KikoTools is unchanged.
Keyboard handling, input ARIA state, matching and insertion remain in consumers.
The stylesheet hash above describes the initial extraction before this addition.
+10
View File
@@ -67,3 +67,13 @@
.ca-tools { --ca-accent: Highlight; --ca-focus: Highlight; --ca-border: ButtonText; --ca-line: CanvasText; }
.ca-tools .ca-switch input:checked::before { background: HighlightText; }
}
.ca-tools.ca-completions { position: fixed; z-index: 10000; min-inline-size: min(240px, calc(100dvw - 16px)); max-inline-size: min(420px, calc(100dvw - 16px)); max-block-size: min(240px, calc(100dvh - 16px)); overflow: auto; border: 1px solid var(--ca-border); border-radius: 6px; box-shadow: 0 6px 20px #0005; box-sizing: border-box; }
.ca-tools.ca-completions[hidden] { display: none; }
.ca-tools.ca-completions .ca-completion-option { display: flex; align-items: flex-start; gap: 8px; padding: 7px 10px; min-block-size: 32px; cursor: pointer; }
.ca-completion-type { flex-shrink: 0; font-size: 11px; color: var(--ca-focus); min-inline-size: 44px; }
.ca-completion-name { min-inline-size: 0; overflow-wrap: anywhere; }
.ca-completion-directory { color: var(--ca-muted); }
.ca-tools.ca-completions [aria-selected="true"] { background: var(--ca-accent); color: #fff; outline: 2px solid var(--ca-focus); outline-offset: -2px; }
.ca-tools.ca-completions [aria-selected="true"] :where(.ca-completion-type, .ca-completion-directory) { color: inherit; }
@media (pointer: coarse) { .ca-tools.ca-completions .ca-completion-option { min-block-size: 40px; } }
+8 -4
View File
@@ -1,16 +1,20 @@
{
"name": "@comfyassets/toolkit",
"version": "0.1.0",
"resolved": "https://codeload.github.com/ComfyAssets/ComfyAssets-Toolkit/tar.gz/66644f5620e10efeaba090dca2d5be54baa2ee14",
"integrity": "sha512-jvDj3iYJHjRCH7nZimoF4KNLwEVdzKVQIOZrMfoLg9aLnHm98R8YvBUOvnlJ+JNrM8/LnD2m30u3ggSTnR0/sw==",
"resolved": "https://codeload.github.com/ComfyAssets/ComfyAssets-Toolkit/tar.gz/b5a34b1d12336cd1bdfd6b890b80db63cbc3c389",
"integrity": "sha512-rhI9xDH0U1WPBq7zuAN4rE6bLWx7ujTM7IIBNXINexN/z4FoQK4jnGXPMtNhW1XbbEpoZPZdExnrJpcRRPZXUw==",
"files": {
"core.js": {
"source": "ui/core.js",
"sha256": "cd10b700441a607cb1d56051cf0a430c0b36f901c0c351d8f4dae067090d8838"
},
"suggestions.js": {
"source": "ui/suggestions.js",
"sha256": "979b7867bcf7c8d5bec2c6631cec4c15a5cbb5774c4eaa554989b954555b9a04"
},
"controls.css": {
"source": "ui/controls.css",
"sha256": "3ae1ca39077ffc3621b86ae7ef77202841fa27061b0e4623f2bb0413b27ecd7b"
"sha256": "9bb8662a5c911537c0577c5b21a861356550ec6235fcf2101d6585cc148efb8d"
},
"LICENSE": {
"source": "LICENSE",
@@ -18,7 +22,7 @@
},
"SOURCE.md": {
"source": "SOURCE.md",
"sha256": "46c226483f1d0c01524b1363435ccb3b1e67c2b0f6ed9161f7f087e49e8ed5a9"
"sha256": "713be4367ac20a587aa7740b985086dc86a866706584de4d56646c6ed0e574c9"
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import { createScope, mountTheme } from './core.js';
let nextList = 0;
export function suggestionPosition(field, popup, viewport) {
const left = Math.max(8, Math.min(field.left, viewport.width - popup.width - 8));
const below = field.bottom + 4;
const top = below + popup.height <= viewport.height - 8 ? below : field.top - popup.height - 4;
return { left, top: Math.max(8, Math.min(top, viewport.height - popup.height - 8)) };
}
export function createSuggestionView(scope, settings, { label = 'Suggestions' } = {}) {
const element = document.createElement('div');
element.className = 'ca-tools ca-completions';
do { element.id = `ca-completions-${++nextList}`; }
while (document.getElementById(element.id));
element.role = 'listbox';
element.setAttribute('aria-label', label);
element.hidden = true;
document.body.append(element);
mountTheme(element, settings, scope);
let optionsScope;
scope.cleanup(() => { optionsScope?.dispose(); element.remove(); });
return {
element,
hide() {
optionsScope?.dispose();
element.hidden = true;
element.replaceChildren();
},
render(suggestions, onSelect, onInsert) {
optionsScope?.dispose();
optionsScope = createScope();
element.replaceChildren();
suggestions.forEach((item, index) => {
const option = document.createElement('div');
option.className = 'ca-completion-option';
option.id = `${element.id}-${index}`;
option.role = 'option';
option.setAttribute('aria-selected', 'false');
const badge = document.createElement('span');
badge.className = 'ca-completion-type';
badge.textContent = item.type === 'embedding' ? '[EMB]' : item.type === 'lora' ? '[LORA]' : '[TAG]';
const name = document.createElement('span');
name.className = 'ca-completion-name';
const path = item.type === 'embedding' ? item.fullPath : null;
if (path?.includes('/')) {
const directory = document.createElement('span');
directory.className = 'ca-completion-directory';
directory.textContent = path.slice(0, path.lastIndexOf('/') + 1);
name.append(directory, document.createTextNode(path.slice(path.lastIndexOf('/') + 1)));
} else name.textContent = item.name;
if (item.type) option.append(badge);
option.append(name);
optionsScope.listen(option, 'pointerdown', event => event.preventDefault());
optionsScope.listen(option, 'pointermove', () => onSelect(index));
optionsScope.listen(option, 'click', () => onInsert(item));
element.append(option);
});
element.hidden = false;
},
select(index) {
const options = [...element.children];
options.forEach((option, position) => option.setAttribute('aria-selected', String(position === index)));
options[index]?.scrollIntoView({ block: 'nearest' });
return options[index]?.id;
},
position(field) {
const popup = element.getBoundingClientRect();
const { left, top } = suggestionPosition(field, popup, { width: innerWidth, height: innerHeight });
element.style.left = `${left}px`;
element.style.top = `${top}px`;
},
};
}