Walking through the AI workbench as a user: the task head held nine loose controls that wrapped to four or five lines with the side column open. It is now two lines whatever the width, with rename, privacy, links and remove in a menu. The stages and what a task made share one line over the timeline, a failed step is a mark instead of red text, New task opens at once, and a missing label in the Ways panel is filled in. The walk also caught AMB not starting at all on one page load. ComfyUI imports every script under web/ on its own and at once, so any module of the big import cycle can run first; ui_ai_workbench.js wrote to a const of ui_ai_results.js as it loaded, which failed when that module was entered first. The value now lives in a module that imports nothing, and tests/module_cycles.mjs refuses the pattern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.0 KiB
JavaScript
80 lines
3.0 KiB
JavaScript
/**
|
|
* A small menu that opens under a button (the workflow card's ⋯, the Workflows page's
|
|
* filter): rows that run an action, rows ticked as chosen, and headings. One is open at a
|
|
* time; a press outside it, Esc, scrolling or a chosen row closes it.
|
|
*/
|
|
|
|
let openMenu = null;
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/** Closes the open menu, if any. */
|
|
export function closePopupMenu() {
|
|
openMenu?.();
|
|
}
|
|
|
|
/**
|
|
* Opens `items` under `anchor`: { heading } or { label, run, checked?, danger?, title? }. A row with
|
|
* `checked` (true or false) shows a tick when true. Pressing the anchor again closes it.
|
|
*/
|
|
export function openPopupMenu(anchor, items) {
|
|
const wasOpenHere = openMenu?.anchor === anchor;
|
|
closePopupMenu();
|
|
if (wasOpenHere) return;
|
|
const menu = el('div', 'anomalous-popup-menu');
|
|
menu.setAttribute('role', 'menu');
|
|
for (const item of items) {
|
|
if (item.heading) {
|
|
menu.append(el('div', 'anomalous-popup-menu-heading', item.heading));
|
|
continue;
|
|
}
|
|
const row = el('button', `anomalous-popup-menu-item${item.danger ? ' is-danger' : ''}`);
|
|
row.type = 'button';
|
|
if (item.title) row.title = item.title;
|
|
row.setAttribute('role', item.checked === undefined ? 'menuitem' : 'menuitemradio');
|
|
if (item.checked !== undefined) {
|
|
row.setAttribute('aria-checked', String(Boolean(item.checked)));
|
|
row.append(el('span', 'anomalous-popup-menu-tick', item.checked ? '✓' : ''));
|
|
}
|
|
row.append(el('span', 'anomalous-popup-menu-label', item.label));
|
|
row.onclick = (event) => {
|
|
event.stopPropagation();
|
|
close();
|
|
item.run?.();
|
|
};
|
|
menu.append(row);
|
|
}
|
|
document.body.append(menu);
|
|
const box = anchor.getBoundingClientRect();
|
|
const { width, height } = menu.getBoundingClientRect();
|
|
const left = Math.min(Math.max(8, box.right - width), window.innerWidth - width - 8);
|
|
const below = box.bottom + 4;
|
|
menu.style.left = `${left}px`;
|
|
menu.style.top = `${below + height > window.innerHeight - 8 ? Math.max(8, box.top - height - 4) : below}px`;
|
|
|
|
const onPointer = (event) => {
|
|
if (!menu.contains(event.target) && !anchor.contains(event.target)) close();
|
|
};
|
|
const onKey = (event) => {
|
|
if (event.key !== 'Escape') return;
|
|
event.stopPropagation();
|
|
close();
|
|
};
|
|
const close = () => {
|
|
menu.remove();
|
|
window.removeEventListener('pointerdown', onPointer, true);
|
|
window.removeEventListener('keydown', onKey, true);
|
|
window.removeEventListener('scroll', close, true);
|
|
if (openMenu === close) openMenu = null;
|
|
};
|
|
close.anchor = anchor;
|
|
window.addEventListener('pointerdown', onPointer, true);
|
|
window.addEventListener('keydown', onKey, true);
|
|
window.addEventListener('scroll', close, true);
|
|
openMenu = close;
|
|
} |