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:
@@ -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.
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user