Media bin: controls pinned at the top, Delete ends Select mode

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-11-08 16:05:03 +02:00
co-authored by Claude Sonnet 5.5
parent 1b4870c499
commit 4cf2f58bf6
4 changed files with 30 additions and 4 deletions
+3
View File
@@ -408,6 +408,9 @@
.cx-stack { display: flex; flex-direction: column; min-inline-size: 0; }
.cx-stack > * { min-inline-size: 0; }
.cx-stack-none { gap: 0; }
/* Pinned to the top of the scrolling panel body: the controls stay in reach while the list under them scrolls.
The body pads its top, so the stack reaches back over that padding and gives it back inside. */
.cx-stack-sticky { position: sticky; inset-block-start: calc(-1 * var(--sp-7)); z-index: 1; margin-block-start: calc(-1 * var(--sp-7)); padding-block: var(--sp-7) var(--sp-4); background: var(--elev-1); }
.cx-stack-sm { gap: var(--sp-5); --cx-gap: var(--sp-5); }
.cx-stack-md { gap: var(--sp-8); --cx-gap: var(--sp-8); }
.cx-stack-lg { gap: var(--sp-10); }
+2 -2
View File
@@ -126,8 +126,8 @@ define("panel", "zone", panel("zone"));
// Without it the shell reaches for the document to make a div of its own, and a
// div the shell made is a div the shell has to lay out -- which is the one thing
// that is supposed to live only here.
define("region", "stack", ({ children = [], gap = "md", label, fill = false } = {}) => {
const node = el("div", { cls: ["cx-stack", `cx-stack-${gap}`, fill ? "cx-stack-fill" : null],
define("region", "stack", ({ children = [], gap = "md", label, fill = false, sticky = false } = {}) => {
const node = el("div", { cls: ["cx-stack", `cx-stack-${gap}`, fill ? "cx-stack-fill" : null, sticky ? "cx-stack-sticky" : null],
attrs: { "aria-label": label } });
const set = (next = []) => {
// A redraw replaces the field under the caret (an arrow press commits, and the panel redraws): the caret moves to
+22
View File
@@ -218,3 +218,25 @@ menuTest("an already pinned picture is unpinned, scenes keep it, new scenes no l
menuTest("reference for all scenes skips scenes that have it or are full, pinned counts as having it", cat,
{ fixed_references: { z: 1 }, scenes: [{ id: "s1" }, { id: "s2", references: ["m1"] }, { id: "s3", references: Array.from({ length: 15 }, (_, i) => `r${i}`) }, { id: "s4", references: ["a"] }] },
"reference for all scenes", (d) => assert.deepEqual(d.scenes.map((s) => s.references), [["m1"], ["m1"], Array.from({ length: 15 }, (_, i) => `r${i}`), ["a", "m1"]]));
test("Delete after picking files ends Select mode, and the bin's controls are pinned at the top", async () => {
const project = { selected: null, project: { fixed_references: {} }, setScene: () => {}, edit: () => {} };
let list = [{ id: "m1", name: "cat.png", kind: "image" }, { id: "m2", name: "dog.png", kind: "image" }];
const api = { media: async () => ({ media: list }), deleteMedia: async (id) => { list = list.filter((m) => m.id !== id); } };
const host = document.createElement("div");
hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: () => () => {} } });
await new Promise((r) => setTimeout(r, 10));
const btn = (t) => [...document.querySelectorAll("button")].find((b) => b.textContent.trim().startsWith(t));
assert.ok(host.querySelector(".cx-stack-sticky button"), "the controls sit in the pinned stack");
document.body.append(host);
btn("Select").click();
host.querySelector(".cx-cell").click();
btn("Delete (1)").click();
await new Promise((r) => setTimeout(r, 10));
[...document.querySelectorAll("button")].filter((b) => b.textContent.trim() === "Delete").pop().click();
await new Promise((r) => setTimeout(r, 30));
assert.equal(host.querySelectorAll(".cx-cell").length, 1);
assert.ok(btn("Select"), "back to Select, not Done");
assert.equal(btn("Delete (0)").hidden, true);
host.remove();
});
+3 -2
View File
@@ -146,12 +146,13 @@ export default {
const ids = [...chosen];
if (!ids.length || !(await c.modal.dialogue({ title: "Delete media", message: `Delete ${ids.length} file${ids.length > 1 ? "s" : ""} from the bin for good? Scenes in other projects that use them will lose them too.`, tone: "danger", confirmLabel: "Delete" }).result)) return;
await gone(ids);
selecting = false; chosen.clear(); draw(true); // Delete ends Select mode
} });
const controls = { sort, view: view, cols: tileSize };
bulk.node.hidden = true;
const clearRefs = c.button.sm({ label: "Clear references", tone: "ghost", title: "Stop using every reference picture in every scene (pinned ones stay)", onClick: () => p.edit(clearReferences) });
host.append(c.toolbar.default({ items: [c.text.sm({ text: "Media" })], trailing: [clearRefs, selectBtn, bulk] }).node, drop.node, note.node, seg("Show", FILTERS, "filter").node, sort.node,
view.node, tileSize.node, shelf.node);
const bar = c.region.stack({ gap: "sm", sticky: true, children: [c.toolbar.default({ items: [c.text.sm({ text: "Media" })], trailing: [clearRefs, selectBtn, bulk] }), drop, note, seg("Show", FILTERS, "filter"), sort, view, tileSize] });
host.append(bar.node, shelf.node);
let drawn = "";
function adopt() {