pixelkiln 0.80.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,7 +84,7 @@ section.project { border-bottom:1px solid var(--line-strong); background:var(--p
84
84
  .card:hover { border-color:var(--line-strong); }
85
85
  .card.active { border-color:var(--accent); box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent); }
86
86
  .card.ghost { border-style:dashed; background:transparent; }
87
- .cell { height:132px; display:grid; place-items:center; background-color:var(--stage-bg);
87
+ .cell { height:132px; flex:none; display:grid; place-items:center; background-color:var(--stage-bg);
88
88
  background-image:var(--stage-pattern); background-size:12px 12px; background-position:0 0,6px 6px;
89
89
  overflow:hidden; position:relative; padding:8px; }
90
90
  .card.ghost .cell { background-image:none; background-color:var(--panel-deep); color:var(--dim); font-size:12px; }
@@ -484,3 +484,44 @@ td.col-on { background:rgba(255,107,53,.08); }
484
484
  .dtabs .tablist button.on { color:var(--accent-soft); border-bottom-color:var(--accent); }
485
485
  .dtabs .tabpanel[hidden] { display:none; }
486
486
  .dtabs .tabpanel > section.meta:first-child { border-top:0; }
487
+
488
+ /* ---- View menu, card sizes, tag chips, shortcut sheet ---- */
489
+ .bar { flex-wrap:nowrap; }
490
+ .bar > button, .bar > .editing { white-space:nowrap; flex:none; }
491
+ .bar .search { flex:0 1 360px; min-width:180px; }
492
+ .menu { position:relative; }
493
+ .menu > summary { list-style:none; cursor:pointer; padding:6px 11px; border:1px solid var(--line); white-space:nowrap; }
494
+ .menu > summary::-webkit-details-marker { display:none; }
495
+ .menu > summary::after { content:' ▾'; color:var(--dim); font-size:11px; }
496
+ .menu[open] > summary { border-color:var(--accent); color:var(--accent-soft); }
497
+ .menu-panel { position:absolute; right:0; top:calc(100% + 6px); z-index:15; width:300px; display:grid; gap:10px;
498
+ padding:12px; background:var(--panel); border:1px solid var(--line-strong); box-shadow:0 10px 30px rgba(0,0,0,.45); }
499
+ .menu-row { display:grid; grid-template-columns:78px 1fr; align-items:center; gap:10px; font-size:12.5px; }
500
+ .menu-row > span:first-child { color:var(--dim); font:600 11.5px/1 var(--mono); }
501
+ .menu-row.check { display:flex; gap:8px; color:var(--text); }
502
+ .menu-row[hidden] { display:none; }
503
+ .menu-row select { width:100%; }
504
+ .seg { display:inline-flex; }
505
+ .seg button { padding:4px 12px; font:600 12px/1 var(--mono); }
506
+ .seg button + button { border-left:0; }
507
+ .seg button.on { background:var(--panel-deep); color:var(--accent-soft); border-color:var(--accent); }
508
+ #keys { width:32px; padding:6px 0; font:700 13px/1 var(--mono); }
509
+ body[data-size="s"] .grid { grid-template-columns:repeat(auto-fill, minmax(132px, 1fr)); gap:8px; }
510
+ body[data-size="s"] .cell { height:96px; }
511
+ /* A card not yet drawn holds this size (content-visibility), so it has to match the card size. */
512
+ body[data-size="s"] .card { contain-intrinsic-size:auto 132px auto 160px; }
513
+ body[data-size="l"] .card { contain-intrinsic-size:auto 260px auto 275px; }
514
+ body[data-size="s"] .cbody { padding:6px 8px; }
515
+ body[data-size="l"] .grid { grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); }
516
+ body[data-size="l"] .cell { height:210px; }
517
+ .chip.tag { font-family:var(--mono); }
518
+ .keys-sheet { background:var(--panel); border:1px solid var(--accent); width:min(620px, calc(100vw - 32px)); max-height:calc(100vh - 64px);
519
+ overflow-y:auto; padding:16px 18px; display:grid; gap:14px; }
520
+ .keys-head { display:flex; align-items:center; justify-content:space-between; }
521
+ .keys-sheet h3 { margin:0; font:650 14px/1.2 var(--mono); }
522
+ .keys-sheet h4 { margin:0 0 6px; font:650 12px/1 var(--mono); color:var(--accent-soft); }
523
+ .keys-sheet dl { display:grid; grid-template-columns:150px 1fr; gap:6px 16px; margin:0; font-size:13px; }
524
+ .keys-sheet dt { white-space:nowrap; }
525
+ .keys-sheet dd { margin:0; color:var(--dim); }
526
+ .keys-sheet kbd { font:600 11.5px/1 var(--mono); border:1px solid var(--line-strong); border-bottom-width:2px; padding:2px 6px; }
527
+ @media (max-width: 900px) { .bar { flex-wrap:wrap; } }
@@ -47,16 +47,26 @@
47
47
  onion: false,
48
48
  selecting: false,
49
49
  selected: /* @__PURE__ */ new Set(),
50
- drawerTab: "overview"
50
+ drawerTab: "overview",
51
+ tags: /* @__PURE__ */ new Set(),
52
+ size: "m"
51
53
  };
54
+ var CARD_SIZES = {
55
+ s: { box: [112, 80], tile: [48, 36] },
56
+ m: { box: [156, 116], tile: [66, 52] },
57
+ l: { box: [240, 190], tile: [108, 88] }
58
+ };
59
+ var declaredTags = (item) => item.asset && Array.isArray(item.asset.tags) ? item.asset.tags : [];
52
60
  var PREFS_KEY = "pixelkiln.gallery.prefs";
53
- var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion", "drawerTab"];
61
+ var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion", "drawerTab", "size"];
54
62
  function loadPrefs() {
55
63
  try {
56
64
  const saved = JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
57
65
  for (const name of PREF_NAMES) if (typeof saved[name] === typeof ui[name]) ui[name] = saved[name];
58
66
  } catch {
59
67
  }
68
+ if (!CARD_SIZES[ui.size]) ui.size = "m";
69
+ document.body.dataset.size = ui.size;
60
70
  applyBackdrop();
61
71
  }
62
72
  function savePrefs() {
@@ -1197,6 +1207,7 @@
1197
1207
  if (ui.providers.size && !ui.providers.has(item.provider)) return false;
1198
1208
  if (ui.generators.size && !ui.generators.has(item.generator)) return false;
1199
1209
  if (ui.projects.size && !ui.projects.has(item.project ?? "")) return false;
1210
+ if (ui.tags.size && !declaredTags(item).some((t) => ui.tags.has(t))) return false;
1200
1211
  if (!q) return true;
1201
1212
  const hay = [
1202
1213
  item.id,
@@ -1304,7 +1315,19 @@
1304
1315
  chips.append(el("span", "sep"));
1305
1316
  for (const [k, v] of [...byGen].sort()) chips.append(chip(k, v, ui.generators, k));
1306
1317
  }
1307
- if (ui.states.size || ui.providers.size || ui.generators.size || ui.projects.size || ui.q) {
1318
+ const byTag = /* @__PURE__ */ new Map();
1319
+ for (const item of S.snap.items) for (const t2 of declaredTags(item)) byTag.set(t2, (byTag.get(t2) || 0) + 1);
1320
+ for (const t2 of ui.tags) if (!byTag.has(t2)) byTag.set(t2, 0);
1321
+ if (byTag.size) {
1322
+ chips.append(el("span", "sep"));
1323
+ for (const [k, v] of [...byTag].sort()) {
1324
+ const c = chip("#" + k, v, ui.tags, k);
1325
+ c.classList.add("tag");
1326
+ c.title = "Assets tagged “" + k + "” in the manifest";
1327
+ chips.append(c);
1328
+ }
1329
+ }
1330
+ if (ui.states.size || ui.providers.size || ui.generators.size || ui.projects.size || ui.tags.size || ui.q) {
1308
1331
  const clear = el("button", "chip", "clear filters");
1309
1332
  clear.type = "button";
1310
1333
  clear.onclick = () => clearFilters();
@@ -1316,6 +1339,7 @@
1316
1339
  ui.providers.clear();
1317
1340
  ui.generators.clear();
1318
1341
  ui.projects.clear();
1342
+ ui.tags.clear();
1319
1343
  ui.q = "";
1320
1344
  $("q").value = "";
1321
1345
  render();
@@ -1337,7 +1361,7 @@
1337
1361
  img2.src = o.url;
1338
1362
  img2.alt = o.role || item.assetId;
1339
1363
  img2.loading = "lazy";
1340
- const s2 = displayScale(w, h, 66, 52);
1364
+ const s2 = displayScale(w, h, ...CARD_SIZES[ui.size].tile);
1341
1365
  if (s2 >= 1) {
1342
1366
  img2.width = w * s2;
1343
1367
  img2.height = h * s2;
@@ -1352,7 +1376,7 @@
1352
1376
  img.src = shown[0].url;
1353
1377
  img.alt = item.assetId;
1354
1378
  img.loading = "lazy";
1355
- const s = displayScale(w, h, 156, 116);
1379
+ const s = displayScale(w, h, ...CARD_SIZES[ui.size].box);
1356
1380
  if (s >= 1) {
1357
1381
  img.width = w * s;
1358
1382
  img.height = h * s;
@@ -4489,6 +4513,7 @@
4489
4513
  ui.states = new Set(list("state"));
4490
4514
  ui.providers = new Set(list("provider"));
4491
4515
  ui.generators = new Set(list("generator"));
4516
+ ui.tags = new Set(list("tag"));
4492
4517
  ui.projects = new Set(list("project"));
4493
4518
  ui.compare = list("compare").slice(0, 4);
4494
4519
  if (["key", "newest", "oldest", "cost", "size"].includes(params.get("sort") ?? "")) ui.sort = params.get("sort");
@@ -4500,6 +4525,7 @@
4500
4525
  if (ui.states.size) params.set("state", [...ui.states].join(","));
4501
4526
  if (ui.providers.size) params.set("provider", [...ui.providers].join(","));
4502
4527
  if (ui.generators.size) params.set("generator", [...ui.generators].join(","));
4528
+ if (ui.tags.size) params.set("tag", [...ui.tags].join(","));
4503
4529
  if (ui.projects.size) params.set("project", [...ui.projects].join(","));
4504
4530
  if (ui.compare.length) params.set("compare", ui.compare.join(","));
4505
4531
  if (ui.sort !== "key") params.set("sort", ui.sort);
@@ -5061,10 +5087,104 @@
5061
5087
  setTimeout(() => baseId.focus(), 0);
5062
5088
  }
5063
5089
 
5090
+ // src/gallery/client/src/keys.ts
5091
+ var GROUPS = [
5092
+ {
5093
+ title: "Anywhere",
5094
+ keys: [
5095
+ [["/"], "jump to search"],
5096
+ [["?"], "this list"],
5097
+ [["Esc"], "close whatever is open: a sheet, the record, the View menu, or select mode"]
5098
+ ]
5099
+ },
5100
+ {
5101
+ title: "On the grid",
5102
+ keys: [
5103
+ [["click"], "open a record"],
5104
+ [["shift", "click"], "add it to a side-by-side comparison (up to four)"],
5105
+ [["ctrl/⌘", "click"], "select it; the bar at the bottom acts on the selection"],
5106
+ [["hover"], "play a loop"]
5107
+ ]
5108
+ },
5109
+ {
5110
+ title: "With a record open",
5111
+ keys: [
5112
+ [["←", "→"], "step through the records in view"],
5113
+ [[",", "."], "step a loop one frame back or on"],
5114
+ [["space"], "play or pause a loop"],
5115
+ [["←", "→"], "switch tabs, once a tab has focus"]
5116
+ ]
5117
+ },
5118
+ {
5119
+ title: "In the family view",
5120
+ keys: [
5121
+ [["drag"], "turn the sprite on the turntable"],
5122
+ [["←", "→"], "turn it a step, once the turntable has focus"]
5123
+ ]
5124
+ }
5125
+ ];
5126
+ function openShortcuts() {
5127
+ const host = $("dialog-host");
5128
+ host.textContent = "";
5129
+ const wrap = el("div", "dialog");
5130
+ wrap.onclick = (e) => {
5131
+ if (e.target === wrap) host.textContent = "";
5132
+ };
5133
+ const box = el("div", "keys-sheet");
5134
+ box.setAttribute("role", "dialog");
5135
+ box.setAttribute("aria-label", "Keyboard shortcuts");
5136
+ const head = el("div", "keys-head");
5137
+ head.append(el("h3", null, "Keyboard shortcuts"));
5138
+ const close = el("button", null, "Close");
5139
+ close.type = "button";
5140
+ close.onclick = () => {
5141
+ host.textContent = "";
5142
+ };
5143
+ head.append(close);
5144
+ box.append(head);
5145
+ for (const group of GROUPS) {
5146
+ const section2 = el("section");
5147
+ section2.append(el("h4", null, group.title));
5148
+ const dl = el("dl");
5149
+ for (const [keys, what] of group.keys) {
5150
+ const dt = el("dt");
5151
+ keys.forEach((k, i) => {
5152
+ if (i) dt.append(document.createTextNode(k === "click" ? "+" : " "));
5153
+ dt.append(el("kbd", null, k));
5154
+ });
5155
+ dl.append(dt, el("dd", null, what));
5156
+ }
5157
+ section2.append(dl);
5158
+ box.append(section2);
5159
+ }
5160
+ wrap.append(box);
5161
+ host.append(wrap);
5162
+ close.focus();
5163
+ }
5164
+
5064
5165
  // src/gallery/client/src/main.ts
5065
5166
  loadPrefs();
5066
5167
  $("refresh").onclick = refresh;
5067
5168
  $("select").onclick = () => setSelecting(!ui.selecting);
5169
+ $("keys").onclick = openShortcuts;
5170
+ var syncSize = () => {
5171
+ for (const b of $("size").querySelectorAll("button")) b.classList.toggle("on", b.dataset.size === ui.size);
5172
+ };
5173
+ for (const b of $("size").querySelectorAll("button")) {
5174
+ b.onclick = () => {
5175
+ if (!CARD_SIZES[b.dataset.size]) return;
5176
+ ui.size = b.dataset.size;
5177
+ document.body.dataset.size = ui.size;
5178
+ savePrefs();
5179
+ syncSize();
5180
+ render();
5181
+ };
5182
+ }
5183
+ syncSize();
5184
+ document.addEventListener("click", (e) => {
5185
+ const menu = $("view-menu");
5186
+ if (menu.open && !menu.contains(e.target)) menu.open = false;
5187
+ });
5068
5188
  $("playloops").checked = ui.playLoops;
5069
5189
  $("playloops").onchange = (e) => {
5070
5190
  setPlayLoops(e.target.checked);
@@ -5109,7 +5229,18 @@
5109
5229
  $("q").focus();
5110
5230
  return;
5111
5231
  }
5232
+ if (e.key === "?" && !typing) {
5233
+ e.preventDefault();
5234
+ openShortcuts();
5235
+ return;
5236
+ }
5112
5237
  if (e.key === "Escape") {
5238
+ if ($("view-menu").open) {
5239
+ e.preventDefault();
5240
+ $("view-menu").open = false;
5241
+ $("view-menu").querySelector("summary").focus();
5242
+ return;
5243
+ }
5113
5244
  if (typing && document.activeElement?.id === "q") {
5114
5245
  document.activeElement.blur();
5115
5246
  return;
package/dist/index.cjs CHANGED
@@ -17063,26 +17063,35 @@ ${css}</style>
17063
17063
  <span class="project" id="project"></span>
17064
17064
  <span class="grow"></span>
17065
17065
  <label class="search"><input id="q" type="search" placeholder="Search assets, prompts, ids, hashes" autocomplete="off"><kbd>/</kbd></label>
17066
- <select id="sort" aria-label="Sort">
17067
- <option value="key">Sort: style / asset</option>
17068
- <option value="newest">Sort: newest first</option>
17069
- <option value="oldest">Sort: oldest first</option>
17070
- <option value="cost">Sort: cost</option>
17071
- <option value="size">Sort: size</option>
17072
- </select>
17073
- <select id="group" aria-label="Group">
17074
- <option value="style">Group: by style</option>
17075
- <option value="family">Group: by family</option>
17076
- <option value="none">Group: none</option>
17077
- </select>
17066
+ <details class="menu" id="view-menu">
17067
+ <summary title="Sort, group, card size, backdrop, and playback">View</summary>
17068
+ <div class="menu-panel">
17069
+ <label class="menu-row"><span>Sort</span><select id="sort" aria-label="Sort">
17070
+ <option value="key">style / asset</option>
17071
+ <option value="newest">newest first</option>
17072
+ <option value="oldest">oldest first</option>
17073
+ <option value="cost">cost</option>
17074
+ <option value="size">size</option>
17075
+ </select></label>
17076
+ <label class="menu-row"><span>Group</span><select id="group" aria-label="Group">
17077
+ <option value="style">by style</option>
17078
+ <option value="family">by family</option>
17079
+ <option value="none">none</option>
17080
+ </select></label>
17081
+ <div class="menu-row"><span>Cards</span><span class="seg" id="size" role="group" aria-label="Card size">
17082
+ <button type="button" data-size="s" title="Small cards: more on screen">S</button><button type="button" data-size="m" title="Medium cards">M</button><button type="button" data-size="l" title="Large cards: sprites drawn bigger">L</button>
17083
+ </span></div>
17084
+ <div class="menu-row"><span>Backdrop</span><span id="backdrop-slot"></span></div>
17085
+ <label class="menu-row check" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> Play every loop</label>
17086
+ <label class="menu-row check" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> Refresh every 5 seconds</label>
17087
+ <label class="menu-row check" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> Notify when a job finishes</label>
17088
+ </div>
17089
+ </details>
17078
17090
  <span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
17079
17091
  <button id="select" type="button" title="Pick several records to generate, regenerate, compare, or tag together (or ctrl/\u2318-click a card)">Select</button>
17080
17092
  <button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
17081
17093
  <button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
17082
- <label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
17083
- <label class="chip" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> play loops</label>
17084
- <span id="backdrop-slot"></span>
17085
- <label class="chip" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> notify</label>
17094
+ <button id="keys" type="button" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">?</button>
17086
17095
  </div>
17087
17096
  <div class="totals" id="totals"></div>
17088
17097
  <div class="chips" id="chips"></div>
@@ -17092,8 +17101,7 @@ ${css}</style>
17092
17101
  <main id="root"></main>
17093
17102
  <div id="selbar" class="selbar" hidden></div>
17094
17103
  <footer>
17095
- Click a sprite for its full record; <kbd>shift</kbd>-click adds it to a side-by-side comparison. <kbd>\u2190</kbd>/<kbd>\u2192</kbd> step through the visible set while a record
17096
- is open, <kbd>,</kbd>/<kbd>.</kbd> step a loop's frames and <kbd>space</kbd> plays it, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
17104
+ Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
17097
17105
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
17098
17106
  Editing is on: saving rewrites the manifest and nothing else</span><span id="foot-gen" hidden>.
17099
17107
  Generation is on under the session budget shown above; every run is the same submit, poll, and fetch as <code>pixelkiln gen</code></span>.