Files
olivv-cs-ComfyUI-FunPack/app/composer/elements/gallery.js
T
digital-garbage 094e056760 composer: the elements
55 element groups across text, buttons, choice, input, slider, layout, status,
overlays, gallery and the wheel picker. Each lands in a registry-driven
catalogue that shows every variant in both themes, so an element that fails to
register is visibly absent rather than quietly missing.

rule_content_is_data iterates the REGISTRY, not a list: a new element is
attacked with markup the moment it calls define(), and one without a demo fails
the suite. That is announcement-not-enumeration applied to the tests.

Fixes found while building: checkbox silently ignored its label (inverted
logic left it with aria-label=toggle), the overlay root set z-index 0 and so
became a stacking context that trapped every overlay it was built to free, and
raising a window left its peers' z stale because z is derived from position --
claim() now notifies a rung when a peer moves.

The wheel highlights the actual sector rather than a box around the label: the
lit shape is the region that picks the item, which is what makes it aimable.
2026-08-28 19:07:23 +03:00

106 lines
4.6 KiB
JavaScript

// Grids of things.
//
// The adaptive one is v4's media bin generalised: it sizes off its CONTAINER,
// not the viewport, so a 280px panel lays out like a 280px panel whether the
// window is 1200px or 3000px wide.
import { define } from "../internals/register.js";
import { el } from "../internals/el.js";
import { applyDensity, recallCols, rememberCols, normaliseCols } from "../internals/density.js";
define("gallery", "adaptive", ({ id = "gallery", items = [], cols, onActivate, onContext, selection = [], empty = "Nothing here yet" } = {}) => {
const chosen = new Set(selection);
const grid = el("div", { cls: "cx-gallery", attrs: { role: "listbox", "aria-multiselectable": "true" } });
applyDensity(grid, cols === undefined ? recallCols(id) : cols);
function draw() {
grid.replaceChildren();
if (!items.length) {
grid.append(el("p", { cls: "cx-list-empty", text: empty }));
return;
}
for (const item of items) {
const cell = el("button", {
cls: ["cx-cell", chosen.has(item.id) ? "cx-on" : null, "cx-focusable"],
attrs: { type: "button", role: "option", "aria-selected": String(chosen.has(item.id)) },
});
const thumb = el("div", { cls: "cx-cell-thumb" });
if (item.thumb) {
// Decorative: the caption below carries the name, so an alt would just
// make a screen reader say everything twice.
thumb.append(el("img", { cls: "cx-cell-img", attrs: { src: item.thumb, alt: "", loading: "lazy" } }));
} else {
thumb.append(el("span", { cls: "cx-cell-glyph", text: item.icon || "▦", attrs: { "aria-hidden": "true" } }));
}
if (item.badge) thumb.append(el("span", { cls: "cx-cell-badge", text: item.badge }));
if (item.duration) thumb.append(el("span", { cls: "cx-cell-duration", text: item.duration }));
cell.append(thumb, el("span", { cls: "cx-cell-name", text: item.label }));
cell.addEventListener("click", () => { if (onActivate) onActivate(item); });
if (onContext) {
cell.addEventListener("contextmenu", (e) => { e.preventDefault(); onContext(item, e); });
}
grid.append(cell);
}
}
draw();
const node = el("div", { cls: "cx-gallery-wrap", children: grid });
return {
node,
get value() { return [...chosen]; },
setValue(next) { chosen.clear(); for (const v of next || []) chosen.add(v); draw(); },
setCols(next) {
const n = normaliseCols(next);
applyDensity(grid, n);
rememberCols(id, n);
},
setItems(next) { items = next; draw(); },
destroy: () => node.remove(),
};
});
/** Large clickable cards: the wizard's "what are you making?" choices. */
define("gallery", "cards", ({ items = [], value, onActivate } = {}) => {
let current = value;
const node = el("div", { cls: "cx-cards", attrs: { role: "radiogroup" } });
const cards = items.map((item) => {
const card = el("button", {
cls: ["cx-card", "cx-focusable"],
attrs: { type: "button", role: "radio", "aria-checked": String(item.id === current) },
children: [
item.icon ? el("span", { cls: "cx-card-icon", text: item.icon, attrs: { "aria-hidden": "true" } }) : null,
el("span", { cls: "cx-card-title", text: item.label }),
item.hint ? el("span", { cls: "cx-hint", text: item.hint }) : null,
].filter(Boolean),
});
card.classList.toggle("cx-on", item.id === current);
card.addEventListener("click", () => {
current = item.id;
for (const { item: i, card: c } of cards) {
c.classList.toggle("cx-on", i.id === current);
c.setAttribute("aria-checked", String(i.id === current));
}
if (onActivate) onActivate(item);
});
node.append(card);
return { item, card };
});
return { node, get value() { return current; }, destroy: () => node.remove() };
});
/** A single scrolling row: filmstrips, segment strips, recent items. */
define("gallery", "strip", ({ items = [], onActivate, label } = {}) => {
const node = el("div", { cls: "cx-strip", attrs: { role: "list", "aria-label": label } });
for (const item of items) {
const cell = el("button", { cls: ["cx-strip-cell", "cx-focusable"],
attrs: { type: "button", role: "listitem", title: item.label } });
if (item.thumb) cell.append(el("img", { cls: "cx-cell-img", attrs: { src: item.thumb, alt: "", loading: "lazy" } }));
else cell.append(el("span", { cls: "cx-cell-glyph", text: item.icon || "▦", attrs: { "aria-hidden": "true" } }));
cell.addEventListener("click", () => { if (onActivate) onActivate(item); });
node.append(cell);
}
return { node, destroy: () => node.remove() };
});