feat: pinned references show a pin on their tile; bin redraws keep keyboard focus; drop the no-op alpha sort helper
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
d5b1211c4b
commit
4bb2ac56cc
@@ -80,7 +80,9 @@ function collection({ items = [], empty = EMPTY, selection = [], onActivate, onC
|
||||
});
|
||||
kept = next;
|
||||
const now = [...host.children];
|
||||
if (now.length !== cells.length || cells.some((cell, i) => cell !== now[i])) host.replaceChildren(...cells);
|
||||
// a rebuilt tile swaps in place when every other tile stays where it is, so a tile with the keyboard focus keeps it
|
||||
if (now.length === cells.length && cells.every((cell, i) => cell === now[i] || !cell.parentNode)) cells.forEach((cell, i) => { if (cell !== now[i]) now[i].replaceWith(cell); });
|
||||
else if (now.length !== cells.length || cells.some((cell, i) => cell !== now[i])) host.replaceChildren(...cells);
|
||||
}
|
||||
draw();
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ define("region", "stack", ({ children = [], gap = "md", label, fill = false } =
|
||||
// document node in through the one element that takes children late.
|
||||
const kids = next.filter(Boolean).map((c) => nodeOf(c, "region.stack"));
|
||||
swapping.depth += 1;
|
||||
try { node.replaceChildren(...kids); } finally { swapping.depth -= 1; }
|
||||
try { if (kids.length !== node.childNodes.length || kids.some((k, i) => k !== node.childNodes[i])) node.replaceChildren(...kids); } finally { swapping.depth -= 1; }
|
||||
const after = at >= 0 ? twins() : [], twin = after.length === before.length && after[at]; // a field appearing or going: no guessing which
|
||||
if (!twin || twin === was) return;
|
||||
if ("committed" in was.dataset && was.value !== was.dataset.committed && twin.value === was.dataset.committed) twin.value = was.value;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Shortcuts: the library of trigger -> replacement phrases, expanded into a prompt when it is generated.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { alpha } from "../../shell/alpha.js";
|
||||
|
||||
const lines = (text) => String(text || "").split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
|
||||
@@ -62,7 +61,7 @@ export function shortcuts(app, own) {
|
||||
|
||||
function draw(lib) {
|
||||
if (!alive) return;
|
||||
const items = [...(lib.shortcuts || [])].sort((x, y) => alpha(x.category || "", y.category || "") || alpha(x.name, y.name)) // by name: the pause mark in a label must not decide the order
|
||||
const items = [...(lib.shortcuts || [])].sort((x, y) => (x.category || "").localeCompare(y.category || "") || x.name.localeCompare(y.name)) // by name: the pause mark in a label must not decide the order
|
||||
.map((s) => ({ id: s.name, label: `${s.enabled ? "" : "⏸ "}${s.name}`, hint: s.triggers.join(", "), keywords: `${s.category} ${s.sub_category} ${s.replacements.join(" ")}`, group: s.category || "" }));
|
||||
const query = box ? box.query : ""; // what was typed in the search box survives a save, a delete, an import
|
||||
box = c.filterList.md({ items, query, placeholder: "Search shortcuts", empty: "No shortcuts yet. Add one with + New.", onChange: (id) => edit((lib.shortcuts || []).find((s) => s.name === id)) });
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// What sits around the Story box: saved templates (scenes + variables), the $variables list, and the shortcut picker.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { alpha } from "../../shell/alpha.js";
|
||||
import { applyStory, storyRoots } from "./story.js";
|
||||
|
||||
const clone = (x) => JSON.parse(JSON.stringify(x));
|
||||
@@ -89,7 +88,7 @@ export async function pickShortcut(app) {
|
||||
if (!all.length) { c.toast.warn({ text: "No shortcuts yet. Add some in the Shortcuts tab." }); return null; }
|
||||
return new Promise((resolve) => {
|
||||
const items = all.map((s) => ({ id: s.name, label: s.name, hint: (s.replacements || [])[0] || "", keywords: `${s.triggers.join(" ")} ${s.category} ${s.sub_category}`, group: [s.category, s.sub_category].filter(Boolean).join(" › ") }))
|
||||
.sort((x, y) => alpha(x.group, y.group) || alpha(x.label, y.label));
|
||||
.sort((x, y) => String(x.group).localeCompare(String(y.group)) || String(x.label).localeCompare(String(y.label)));
|
||||
let chosen = null;
|
||||
const win = c.modal.generic({ title: "Add shortcut", size: "md", onClose: () => resolve(chosen),
|
||||
body: c.filterList.md({ items, placeholder: "Search shortcuts", empty: "Nothing matches.", onChange: (id) => { chosen = all.find((s) => s.name === id).triggers[0]; win.close("done"); } }) });
|
||||
|
||||
@@ -58,7 +58,7 @@ test("a changed pin repaints the slot numbers on the tiles even when the list of
|
||||
hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: (cb) => { fire = cb; return () => {}; } } });
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const flag = (n) => [...[...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes(n)).querySelectorAll(".cx-cell-flag")].map((x) => x.textContent).join();
|
||||
assert.deepEqual([flag("cat.png"), flag("dog.png")], ["R2", "R1"], "pinned at 2, the other takes the free number");
|
||||
assert.deepEqual([flag("cat.png"), flag("dog.png")], ["📌R2", "R1"], "pinned at 2 (marked with a pin), the other takes the free number");
|
||||
delete fixed.m1;
|
||||
fire("project");
|
||||
assert.deepEqual([flag("cat.png"), flag("dog.png")], ["R1", "R2"], "unpinned: back to list order");
|
||||
@@ -106,3 +106,19 @@ test("toggling a reference leaves the other tiles untouched, so the bin does not
|
||||
assert.notEqual(after[1], before[1], "dog.png is the one that changed: it gained its R1 mark");
|
||||
assert.equal(after[1].querySelector(".cx-cell-flag").textContent, "R1");
|
||||
});
|
||||
|
||||
test("redrawing the bin keeps keyboard focus on the tile that has it", async () => {
|
||||
const project = { selected: { id: "s1", references: [] }, project: {}, setScene: () => {} };
|
||||
const api = { media: async () => ({ media: [{ id: "m1", name: "cat.png", kind: "image" }, { id: "m2", name: "dog.png", kind: "image" }] }) };
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
let fire;
|
||||
hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: (cb) => { fire = cb; return () => {}; } } });
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const tile = [...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes("dog.png"));
|
||||
tile.focus(); const was = document.activeElement === tile;
|
||||
project.selected.references = ["m1"];
|
||||
fire("project");
|
||||
const now = [...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes("dog.png")); assert.ok(was, "focusable"); assert.equal(now, tile, "same tile node"); assert.equal(document.activeElement, tile, "the focused tile survives the redraw");
|
||||
host.remove();
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ export default {
|
||||
if (g.setCols) g.setCols(Number(show.cols));
|
||||
// what the selected scene uses is marked on its tile, so a reference can be told from the rest
|
||||
const flags = (m) => [sc && sc.source_image === m.id ? { text: "◐", title: "The selected scene's resolution source" } : null,
|
||||
refs.includes(m.id) ? { text: `R${nums[refs.indexOf(m.id)]}`, title: `Reference slot ${nums[refs.indexOf(m.id)]} for the selected scene` } : null].filter(Boolean);
|
||||
refs.includes(m.id) ? { text: `${plan.pins[m.id] ? "📌" : ""}R${nums[refs.indexOf(m.id)]}`, title: `Reference slot ${nums[refs.indexOf(m.id)]} for the selected scene${plan.pins[m.id] ? ", pinned to every scene" : ""}` } : null].filter(Boolean);
|
||||
g.setItems(shown.map((m) => ({ id: m.id, label: m.name, thumb: m.kind === "audio" ? undefined : `${base(m.id)}/thumb`, badge: m.kind, flags: flags(m) })));
|
||||
g.setValue(selecting ? [...chosen] : sc ? [sc.source_image, ...refs].filter(Boolean) : []);
|
||||
selectBtn.setLabel(selecting ? "Done" : "Select");
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Alphabetical, ignoring case and accents; two names that differ only in case fall back to the exact text so the order never shuffles.
|
||||
export const alpha = (a, b) => String(a).localeCompare(String(b), undefined, { sensitivity: "base" }) || String(a).localeCompare(String(b));
|
||||
Reference in New Issue
Block a user