@cloudinary/asset-management-mcp 0.9.1 → 0.9.3

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.
Files changed (47) hide show
  1. package/README.md +12 -15
  2. package/bin/mcp-server.js +288 -369
  3. package/bin/mcp-server.js.map +13 -13
  4. package/esm/landing-page.d.ts.map +1 -1
  5. package/esm/landing-page.js +9 -3
  6. package/esm/landing-page.js.map +1 -1
  7. package/esm/lib/config.d.ts +3 -3
  8. package/esm/lib/config.js +3 -3
  9. package/esm/mcp-server/apps/app-shared.d.ts +6 -5
  10. package/esm/mcp-server/apps/app-shared.d.ts.map +1 -1
  11. package/esm/mcp-server/apps/app-shared.js +134 -14
  12. package/esm/mcp-server/apps/app-shared.js.map +1 -1
  13. package/esm/mcp-server/apps/asset-details-app.d.ts.map +1 -1
  14. package/esm/mcp-server/apps/asset-details-app.js +7 -14
  15. package/esm/mcp-server/apps/asset-details-app.js.map +1 -1
  16. package/esm/mcp-server/apps/asset-gallery-app.d.ts.map +1 -1
  17. package/esm/mcp-server/apps/asset-gallery-app.js +99 -306
  18. package/esm/mcp-server/apps/asset-gallery-app.js.map +1 -1
  19. package/esm/mcp-server/apps/asset-upload-app.d.ts.map +1 -1
  20. package/esm/mcp-server/apps/asset-upload-app.js +29 -24
  21. package/esm/mcp-server/apps/asset-upload-app.js.map +1 -1
  22. package/esm/mcp-server/apps/config.d.ts.map +1 -1
  23. package/esm/mcp-server/apps/config.js +1 -2
  24. package/esm/mcp-server/apps/config.js.map +1 -1
  25. package/esm/mcp-server/apps/extensions.d.ts.map +1 -1
  26. package/esm/mcp-server/apps/extensions.js +6 -1
  27. package/esm/mcp-server/apps/extensions.js.map +1 -1
  28. package/esm/mcp-server/cli/serve/impl.js +1 -1
  29. package/esm/mcp-server/cli/serve/impl.js.map +1 -1
  30. package/esm/mcp-server/mcp-server.js +1 -1
  31. package/esm/mcp-server/server.js +1 -1
  32. package/esm/types/bigint.d.ts.map +1 -1
  33. package/esm/types/bigint.js +4 -3
  34. package/esm/types/bigint.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/landing-page.ts +9 -3
  37. package/src/lib/config.ts +3 -3
  38. package/src/mcp-server/apps/app-shared.ts +135 -14
  39. package/src/mcp-server/apps/asset-details-app.ts +7 -13
  40. package/src/mcp-server/apps/asset-gallery-app.ts +99 -305
  41. package/src/mcp-server/apps/asset-upload-app.ts +29 -23
  42. package/src/mcp-server/apps/config.ts +1 -2
  43. package/src/mcp-server/apps/extensions.ts +6 -1
  44. package/src/mcp-server/cli/serve/impl.ts +1 -1
  45. package/src/mcp-server/mcp-server.ts +1 -1
  46. package/src/mcp-server/server.ts +1 -1
  47. package/src/types/bigint.ts +18 -17
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  SHARED_CSS_TOKENS,
12
12
  SHARED_CSS_COMPONENTS,
13
+ SHARED_JS_ICONS,
13
14
  SHARED_JS_MCP_CLIENT,
14
15
  SHARED_JS_HELPERS,
15
16
  SHARED_JS_TOOLTIPS,
@@ -37,90 +38,10 @@ const GALLERY_CSS = /* css */ `
37
38
  font-size: var(--cld-font-xxs); color: var(--cld-text3); background: var(--cld-bg3);
38
39
  padding: 2px 8px; border-radius: 20px; font-weight: 500;
39
40
  }
40
- .select-all-btn {
41
- font-size: 12px; color: var(--cld-text2); background: none;
42
- border: 1px solid var(--cld-border); border-radius: var(--cld-radius-sm);
43
- padding: 4px 10px; cursor: pointer; font-family: inherit;
44
- transition: color 0.15s, border-color 0.15s;
45
- }
46
- .select-all-btn:hover { color: var(--cld-accent); border-color: var(--cld-accent); }
47
- .refresh-btn {
48
- background: none; border: 1px solid var(--cld-border); border-radius: var(--cld-radius-sm);
49
- color: var(--cld-text2); cursor: pointer; font-size: 14px; padding: 2px 7px;
50
- line-height: 1; transition: background 0.15s, color 0.15s;
51
- }
52
- .refresh-btn:hover { background: var(--cld-bg3); color: var(--cld-text); }
53
-
54
- /* ── Filter bar ── */
55
- .filter-row {
56
- margin-bottom: var(--cld-sp-md); display: flex; gap: 8px; align-items: center;
57
- }
58
- .filter-text-wrap { position: relative; flex: 1; }
59
- .filter-input {
60
- width: 100%; height: 36px; padding: 0 12px 0 34px;
61
- border: 1px solid var(--cld-border); border-radius: var(--cld-radius);
62
- background: var(--cld-bg); font-size: 12.5px; color: var(--cld-text);
63
- outline: none; font-family: inherit;
64
- transition: border-color 0.18s, box-shadow 0.18s;
65
- }
66
- .filter-input::placeholder { color: var(--cld-text3); }
67
- .filter-input:focus {
68
- border-color: var(--cld-accent);
69
- box-shadow: 0 0 0 3px rgba(52,72,197,0.1);
70
- }
71
- [data-theme="dark"] .filter-input:focus { box-shadow: 0 0 0 3px rgba(13,154,255,0.15); }
72
- .filter-icon {
73
- position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
74
- color: var(--cld-text3); pointer-events: none; display: flex; align-items: center;
75
- }
76
- .filter-clear {
77
- position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
78
- background: none; border: none; color: var(--cld-text3); cursor: pointer;
79
- font-size: 14px; line-height: 1; padding: 2px 4px; border-radius: 4px;
80
- display: none; font-family: inherit;
81
- }
82
- .filter-clear:hover { color: var(--cld-text); background: var(--cld-border); }
83
- .filter-clear.visible { display: block; }
84
-
85
- /* Aspect-ratio dropdown */
86
- .aspect-dropdown { position: relative; flex-shrink: 0; user-select: none; }
87
- .aspect-btn {
88
- height: 36px; padding: 0 10px; border: 1px solid var(--cld-border);
89
- border-radius: var(--cld-radius); background: var(--cld-bg);
90
- font-size: 12.5px; color: var(--cld-text); cursor: pointer;
91
- display: flex; align-items: center; gap: 6px; white-space: nowrap;
92
- transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
93
- font-family: inherit; outline: none;
94
- }
95
- .aspect-btn:hover { border-color: var(--cld-border2); }
96
- .aspect-btn.active {
97
- border-color: var(--cld-accent); background: var(--cld-accent-bg);
98
- color: var(--cld-accent); font-weight: 600;
99
- }
100
- .aspect-btn-chevron { color: var(--cld-text3); flex-shrink: 0; transition: transform 0.18s; }
101
- .aspect-btn.open .aspect-btn-chevron { transform: rotate(180deg); }
102
- .aspect-menu {
103
- position: absolute; top: calc(100% + 6px); right: 0;
104
- background: var(--cld-bg); border: 1px solid var(--cld-border);
105
- border-radius: 10px; box-shadow: var(--cld-shadow-md);
106
- padding: 4px; min-width: 160px; z-index: 50; display: none;
107
- }
108
- .aspect-menu.open { display: block; }
109
- .aspect-option {
110
- display: flex; align-items: center; gap: 10px;
111
- padding: 7px 10px; border-radius: 6px; font-size: 12.5px;
112
- color: var(--cld-text); cursor: pointer; transition: background 0.18s;
113
- }
114
- .aspect-option:hover { background: var(--cld-bg3); }
115
- .aspect-option.selected { color: var(--cld-accent); font-weight: 600; }
116
- .aspect-opt-icon { color: var(--cld-text3); display: flex; align-items: center; flex-shrink: 0; }
117
- .aspect-option.selected .aspect-opt-icon { color: var(--cld-accent); }
118
- .aspect-check { margin-left: auto; color: var(--cld-accent); opacity: 0; }
119
- .aspect-option.selected .aspect-check { opacity: 1; }
120
- .no-results {
121
- grid-column: 1 / -1; padding: 60px 20px;
122
- text-align: center; color: var(--cld-text3); font-size: 13px;
123
- }
41
+ /* action-btn svg sizing */
42
+ .action-btn svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
43
+ /* select bar svg */
44
+ .bar-btn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
124
45
 
125
46
  /* ── Grid ── */
126
47
  .grid {
@@ -179,18 +100,11 @@ const GALLERY_CSS = /* css */ `
179
100
  opacity: 0; transition: opacity 0.18s; z-index: 4; pointer-events: none;
180
101
  }
181
102
  .card:hover .tags-overlay { opacity: 1; }
182
- .grid.filtering .tags-overlay { opacity: 1; }
183
103
  .tag-overlay {
184
104
  font-size: 10px; color: white;
185
105
  background: rgba(10, 12, 18, 0.55); padding: 2px 7px; border-radius: 20px;
186
106
  backdrop-filter: blur(6px); font-weight: 600; letter-spacing: 0.02em;
187
107
  }
188
- .tag-overlay.tag-match { background: rgba(52, 72, 197, 0.82); }
189
- .tag-overlay mark {
190
- background: rgba(255, 213, 79, 0.5); color: white;
191
- border-radius: 2px; padding: 0 1px;
192
- }
193
-
194
108
  /* Floating action buttons */
195
109
  .card-actions {
196
110
  position: absolute; bottom: 10px; left: 0; right: 0;
@@ -254,18 +168,27 @@ const GALLERY_CSS = /* css */ `
254
168
  }
255
169
 
256
170
  /* ── Multi-select bar ── */
171
+ /* Wrapper takes layout space ONLY when a selection is active. When idle
172
+ * it's display:none so it contributes 0 to scrollHeight (no phantom
173
+ * scrollbar gutter / no extra iframe height). */
174
+ .select-bar-wrap {
175
+ display: none;
176
+ position: sticky; bottom: 0; left: 0; right: 0;
177
+ justify-content: center; pointer-events: none;
178
+ z-index: 100; height: 64px;
179
+ }
180
+ .select-bar-wrap.visible { display: flex; }
257
181
  .select-bar {
258
- position: fixed; bottom: 16px; left: 50%;
259
- transform: translateX(-50%) translateY(80px);
182
+ position: absolute; bottom: 8px;
183
+ transform: translateY(80px);
260
184
  background: #1a1d24; color: white; border-radius: 40px;
261
185
  padding: 0 6px 0 16px; height: 48px;
262
186
  display: flex; align-items: center; gap: 4px;
263
187
  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
264
188
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s;
265
- opacity: 0; pointer-events: none; z-index: 100; white-space: nowrap;
189
+ opacity: 0; pointer-events: none; white-space: nowrap;
266
190
  }
267
- .select-bar.visible { transform: translateX(-50%) translateY(0); opacity: 1; pointer-events: all; }
268
- .select-bar-spacer { height: 72px; }
191
+ .select-bar-wrap.visible .select-bar { transform: translateY(0); opacity: 1; pointer-events: all; }
269
192
  .select-count { font-size: 13px; font-weight: 600; margin-right: 8px; }
270
193
  .bar-btn {
271
194
  height: 36px; padding: 0 14px; border: none; border-radius: 30px;
@@ -306,8 +229,6 @@ var pendingCall = {
306
229
  args: null,
307
230
  };
308
231
  var selected = new Set();
309
- var filterQuery = "";
310
- var aspectFilter = "";
311
232
  var app = new MCPApp({ name: "Cloudinary Asset Gallery", version: "1.0.0" });
312
233
  setupHostContext(app);
313
234
 
@@ -331,33 +252,13 @@ function showToast(msg) {
331
252
  _toastTimer = setTimeout(function() { t.classList.remove("show"); }, 2000);
332
253
  }
333
254
 
334
- function getAspect(r) {
335
- if (!r.width || !r.height) return "";
336
- var ratio = r.width / r.height;
337
- if (ratio > 1.1) return "landscape";
338
- if (ratio < 0.9) return "portrait";
339
- return "square";
340
- }
341
-
342
- function highlightText(text, query) {
343
- if (!query) return esc(text);
344
- var lo = text.toLowerCase();
345
- var idx = lo.indexOf(query);
346
- if (idx === -1) return esc(text);
347
- return esc(text.slice(0, idx))
348
- + "<mark>" + esc(text.slice(idx, idx + query.length)) + "</mark>"
349
- + esc(text.slice(idx + query.length));
350
- }
351
-
352
255
  function updateSelectBar() {
353
- var bar = document.getElementById("select-bar");
256
+ var wrap = document.getElementById("select-bar-wrap");
354
257
  var countEl = document.getElementById("select-count");
355
- if (!bar || !countEl) return;
258
+ if (!wrap || !countEl) return;
356
259
  var n = selected.size;
357
260
  countEl.textContent = n + " selected";
358
- bar.classList.toggle("visible", n > 0);
359
- var spacer = document.getElementById("select-bar-spacer");
360
- if (spacer) spacer.style.display = n > 0 ? "" : "none";
261
+ wrap.classList.toggle("visible", n > 0);
361
262
  var btn = document.getElementById("select-all-btn");
362
263
  if (btn) {
363
264
  var visible = getVisibleIndices();
@@ -424,11 +325,9 @@ function copyAssetUrl(type, idx) {
424
325
  var url = r.secure_url || r.url || "";
425
326
  var copyUrl = type === "optimized" ? optimizedUrl(url, r) : url;
426
327
  if (!copyUrl) return;
427
- try {
428
- navigator.clipboard.writeText(copyUrl).then(function() {
429
- showToast(type === "optimized" ? "\\u2728 Optimized URL copied" : "URL copied");
430
- });
431
- } catch(e) { showError("Copy Failed", String(e)); }
328
+ copyText(copyUrl).then(function() {
329
+ showToast(type === "optimized" ? "\\u2728 Optimized URL copied" : "URL copied");
330
+ }).catch(function(e) { showError("Copy Failed", e && e.message ? e.message : String(e)); });
432
331
  }
433
332
 
434
333
  function downloadOne(idx) {
@@ -450,134 +349,74 @@ function copySelectedUrls(type) {
450
349
  urls.push(type === "optimized" ? optimizedUrl(url, r) : url);
451
350
  });
452
351
  if (!urls.length) return;
453
- try {
454
- navigator.clipboard.writeText(urls.join("\\n")).then(function() {
455
- showToast(urls.length + " " + (type === "optimized" ? "optimized " : "") + "URLs copied");
456
- });
457
- } catch(e) { showError("Copy Failed", String(e)); }
352
+ copyText(urls.join("\\n")).then(function() {
353
+ showToast(urls.length + " " + (type === "optimized" ? "optimized " : "") + "URLs copied");
354
+ }).catch(function(e) { showError("Copy Failed", e && e.message ? e.message : String(e)); });
458
355
  }
459
356
 
460
- function downloadSelected() {
461
- var count = 0;
357
+ async function downloadSelected() {
358
+ if (selected.size === 0) return;
359
+
360
+ var picks = [];
462
361
  selected.forEach(function(i) {
463
362
  var r = allResources[i];
464
- if (!r) return;
465
- var url = r.secure_url || r.url || "";
466
- var dl = downloadUrl(url, r);
467
- if (dl) { app._rpc("ui/open-link", { url: dl }); count++; }
363
+ if (r && r.public_id) picks.push(r);
468
364
  });
469
- if (count) showToast("Downloading " + count + " asset" + (count > 1 ? "s" : ""));
470
- }
471
-
472
- function handleFilter() {
473
- var input = document.getElementById("filter-input");
474
- filterQuery = input ? input.value.trim().toLowerCase() : "";
475
-
476
- var clearBtn = document.getElementById("filter-clear");
477
- if (clearBtn) clearBtn.classList.toggle("visible", filterQuery.length > 0);
478
-
479
- var aspectBtn = document.getElementById("aspect-btn");
480
- if (aspectBtn) aspectBtn.classList.toggle("active", aspectFilter !== "");
481
-
482
- var anyFilter = filterQuery.length > 0 || aspectFilter !== "";
483
- var grid = document.getElementById("gallery-grid");
484
- if (grid) grid.classList.toggle("filtering", anyFilter);
365
+ if (!picks.length) return;
366
+
367
+ var requestBody = {
368
+ mode: "create",
369
+ target_format: "zip",
370
+ keep_derived: true,
371
+ target_public_id: "mcp-gallery-archive-" + Date.now(),
372
+ fully_qualified_public_ids: picks.map(function(r) {
373
+ return (r.resource_type || "image") + "/" + (r.type || "upload") + "/" + r.public_id;
374
+ }),
375
+ };
376
+
377
+ var btn = document.getElementById("bar-download");
378
+ var origLabel = btn ? btn.innerHTML : "";
379
+ if (btn) { btn.innerHTML = "Creating archive\\u2026"; btn.disabled = true; }
485
380
 
486
- var visibleCount = 0;
487
- for (var i = 0; i < allResources.length; i++) {
488
- var r = allResources[i];
489
- var card = document.getElementById("card-" + i);
490
- if (!card) continue;
381
+ try {
382
+ var res = await app.callServerTool({
383
+ name: "generate-archive",
384
+ arguments: {
385
+ resource_type: "all",
386
+ RequestBody: requestBody,
387
+ },
388
+ });
491
389
 
492
- var name = (r.public_id || r.filename || "").toLowerCase();
493
- var tags = r.tags || [];
494
- var textMatch = !filterQuery
495
- || name.indexOf(filterQuery) !== -1
496
- || tags.some(function(t) { return t.toLowerCase().indexOf(filterQuery) !== -1; });
497
-
498
- var aspectMatch = !aspectFilter || getAspect(r) === aspectFilter;
499
- var match = textMatch && aspectMatch;
500
- card.style.display = match ? "" : "none";
501
-
502
- var tagsEl = document.getElementById("tags-overlay-" + i);
503
- if (tagsEl && tags.length) {
504
- var maxOv = 3;
505
- var matchedTags = [];
506
- var otherTags = [];
507
- for (var ti = 0; ti < tags.length; ti++) {
508
- var isMatch = filterQuery && tags[ti].toLowerCase().indexOf(filterQuery) !== -1;
509
- if (isMatch) matchedTags.push(tags[ti]);
510
- else otherTags.push(tags[ti]);
511
- }
512
- var shown = matchedTags.slice();
513
- var remaining = maxOv - shown.length;
514
- if (remaining > 0) shown = shown.concat(otherTags.slice(0, remaining));
515
- var hidden = tags.length - shown.length;
516
- var hiddenTags = tags.filter(function(t) { return shown.indexOf(t) === -1; });
517
- tagsEl.innerHTML = shown.map(function(t) {
518
- var matched = filterQuery && t.toLowerCase().indexOf(filterQuery) !== -1;
519
- return '<span class="tag-overlay' + (matched ? ' tag-match' : '') + '">' + highlightText(t, filterQuery) + '</span>';
520
- }).join("") + (hidden > 0 ? '<span class="tag-overlay" title="' + esc(hiddenTags.join(", ")) + '">+' + hidden + '</span>' : '');
390
+ var data = ingestResult(res);
391
+ if (data && (data._error || data._parseError)) {
392
+ showError("Archive Failed", unwrapApiError(data._message));
393
+ return;
521
394
  }
522
-
523
- if (match) visibleCount++;
524
- }
525
-
526
- var badge = document.getElementById("count-badge");
527
- if (badge) {
528
- badge.textContent = anyFilter
529
- ? visibleCount + " of " + allResources.length
530
- : allResources.length + (lastCursor ? "+" : "") + " items";
531
- }
532
-
533
- var noRes = document.getElementById("no-results");
534
- if (visibleCount === 0 && anyFilter) {
535
- if (!noRes && grid) {
536
- noRes = document.createElement("div");
537
- noRes.id = "no-results";
538
- noRes.className = "no-results";
539
- grid.appendChild(noRes);
395
+ var archiveUrl = data && (data.secure_url || data.url);
396
+ if (!archiveUrl) {
397
+ showError("Archive Failed", "No delivery URL returned.");
398
+ return;
540
399
  }
541
- if (noRes) noRes.textContent = "No results" + (filterQuery ? ' for "' + filterQuery + '"' : "") + (aspectFilter ? " in " + aspectFilter + " images" : "");
542
- } else if (noRes) {
543
- noRes.remove();
400
+ try { await copyText(archiveUrl); } catch (e) { /* ignore */ }
401
+ app._rpc("ui/open-link", { url: archiveUrl });
402
+ showToast("Archive saved as raw in Cloudinary \\u2014 opening URL (" + picks.length + " asset" + (picks.length > 1 ? "s" : "") + ")");
403
+ } catch (e) {
404
+ showError("Archive Failed", unwrapApiError(e && e.message ? e.message : String(e)));
405
+ } finally {
406
+ if (btn) { btn.innerHTML = origLabel; btn.disabled = false; }
544
407
  }
545
408
  }
546
409
 
547
- function clearFilter() {
548
- var input = document.getElementById("filter-input");
549
- if (input) input.value = "";
550
- aspectFilter = "";
551
- var label = document.getElementById("aspect-btn-label");
552
- if (label) label.textContent = "All orientations";
553
- document.querySelectorAll(".aspect-option").forEach(function(o) {
554
- o.classList.toggle("selected", o.getAttribute("data-value") === "");
555
- });
556
- handleFilter();
557
- }
558
-
559
- function toggleAspectMenu(e) {
560
- e.stopPropagation();
561
- var btn = document.getElementById("aspect-btn");
562
- var menu = document.getElementById("aspect-menu");
563
- if (!btn || !menu) return;
564
- var open = menu.classList.toggle("open");
565
- btn.classList.toggle("open", open);
566
- }
567
-
568
- function selectAspect(val) {
569
- aspectFilter = val;
570
- var labels = { "": "All orientations", landscape: "Landscape", portrait: "Portrait", square: "Square" };
571
- var label = document.getElementById("aspect-btn-label");
572
- if (label) label.textContent = labels[aspectFilter] || "All orientations";
573
- document.querySelectorAll(".aspect-option").forEach(function(o) {
574
- o.classList.toggle("selected", o.getAttribute("data-value") === aspectFilter);
575
- });
576
- var menu = document.getElementById("aspect-menu");
577
- var btn = document.getElementById("aspect-btn");
578
- if (menu) menu.classList.remove("open");
579
- if (btn) btn.classList.remove("open");
580
- handleFilter();
410
+ function unwrapApiError(raw) {
411
+ if (!raw) return "Unknown error.";
412
+ var msg = String(raw);
413
+ try {
414
+ if (msg.charAt(0) === "{") {
415
+ var parsed = JSON.parse(msg);
416
+ msg = (parsed && parsed.error && parsed.error.message) || msg;
417
+ }
418
+ } catch (e) { /* keep raw */ }
419
+ return msg;
581
420
  }
582
421
 
583
422
  function render() {
@@ -597,41 +436,12 @@ function render() {
597
436
  h += '<h1>Results</h1>';
598
437
  h += '<span class="count-badge" id="count-badge">' + allResources.length + (lastCursor ? "+" : "") + ' items</span>';
599
438
  h += '</div>';
600
- h += '<div style="display:flex;align-items:center;gap:8px">';
601
- h += '<button class="select-all-btn" id="select-all-btn">Select all</button>';
602
- h += '<button class="refresh-btn" id="refresh-gallery" title="Refresh">\\u21BB</button>';
439
+ h += '<div id="header-actions" style="display:flex;align-items:center;gap:8px">';
440
+ h += '<button class="icon-btn" id="select-all-btn">Select all</button>';
441
+ h += '<button class="icon-btn icon-only" id="refresh-gallery" title="Refresh">' + IC.refresh + '</button>';
603
442
  h += '</div>';
604
443
  h += '</div>';
605
444
 
606
- // Filter bar
607
- h += '<div class="filter-row">';
608
- h += '<div class="filter-text-wrap">';
609
- h += '<span class="filter-icon"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="6.5" cy="6.5" r="4.5"/><path d="M10.5 10.5l3 3"/></svg></span>';
610
- h += '<input class="filter-input" id="filter-input" type="text" placeholder="Filter by filename or tag\\u2026" autocomplete="off" spellcheck="false">';
611
- h += '<button class="filter-clear" id="filter-clear">\\u2715</button>';
612
- h += '</div>';
613
- h += '<div class="aspect-dropdown" id="aspect-dropdown">';
614
- h += '<button class="aspect-btn" id="aspect-btn">';
615
- h += '<span id="aspect-btn-label">All orientations</span>';
616
- h += '<svg class="aspect-btn-chevron" width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,4 6,8 10,4"/></svg>';
617
- h += '</button>';
618
- h += '<div class="aspect-menu" id="aspect-menu">';
619
- var aspects = [
620
- { val: "", label: "All orientations", icon: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="12" height="12" rx="1.5"/></svg>' },
621
- { val: "landscape", label: "Landscape", icon: '<svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="1.5"><rect x=".75" y=".75" width="12.5" height="8.5" rx="1.5"/></svg>' },
622
- { val: "portrait", label: "Portrait", icon: '<svg width="10" height="14" viewBox="0 0 10 14" fill="none" stroke="currentColor" stroke-width="1.5"><rect x=".75" y=".75" width="8.5" height="12.5" rx="1.5"/></svg>' },
623
- { val: "square", label: "Square", icon: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><rect x=".75" y=".75" width="10.5" height="10.5" rx="1.5"/></svg>' },
624
- ];
625
- for (var ai = 0; ai < aspects.length; ai++) {
626
- var ao = aspects[ai];
627
- h += '<div class="aspect-option' + (ao.val === aspectFilter ? ' selected' : '') + '" data-value="' + ao.val + '">';
628
- h += '<span class="aspect-opt-icon">' + ao.icon + '</span>';
629
- h += ao.label;
630
- h += '<svg class="aspect-check" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,6 5,9 10,3"/></svg>';
631
- h += '</div>';
632
- }
633
- h += '</div></div></div>';
634
-
635
445
  // Grid
636
446
  h += '<div class="grid" id="gallery-grid">';
637
447
  for (var i = 0; i < allResources.length; i++) {
@@ -680,8 +490,8 @@ function render() {
680
490
  if (url) {
681
491
  h += '<div class="card-actions">';
682
492
  h += '<button class="action-btn act-original" data-copy-original="' + i + '">Copy URL</button>';
683
- if (rt !== "raw") h += '<button class="action-btn act-optimized" data-copy-optimized="' + i + '">\\u2728 Optimized</button>';
684
- h += '<button class="action-btn act-download" data-download="' + i + '" title="Download">\\u2193</button>';
493
+ if (rt !== "raw") h += '<button class="action-btn act-optimized" data-copy-optimized="' + i + '">' + IC.zap + ' Optimized</button>';
494
+ h += '<button class="action-btn act-download" data-download="' + i + '" title="Download">' + IC.arrowDown + '</button>';
685
495
  h += '</div>';
686
496
  }
687
497
 
@@ -728,24 +538,24 @@ function render() {
728
538
  h += "</div>";
729
539
  }
730
540
 
731
- // Spacer so select bar doesn't cover Load More
732
- h += '<div class="select-bar-spacer" id="select-bar-spacer" style="display:none"></div>';
733
-
734
- // Multi-select bar
541
+ // Multi-select bar (sticky, in-flow wrapper so iframe sizing stays truthful)
542
+ h += '<div class="select-bar-wrap" id="select-bar-wrap">';
735
543
  h += '<div class="select-bar" id="select-bar">';
736
544
  h += '<span class="select-count" id="select-count">0 selected</span>';
737
545
  h += '<div class="bar-divider"></div>';
738
- h += '<button class="bar-btn bar-primary" id="bar-copy-optimized" style="display:none">\\u2728 Copy Optimized</button>';
546
+ h += '<button class="bar-btn bar-primary" id="bar-copy-optimized" style="display:none">' + IC.zap + ' Copy Optimized</button>';
739
547
  h += '<button class="bar-btn bar-secondary" id="bar-copy-original">Copy Original</button>';
740
- h += '<button class="bar-btn bar-secondary" id="bar-download">\\u2193 Download All</button>';
548
+ h += '<button class="bar-btn bar-secondary" id="bar-download">' + IC.arrowDown + ' Download Selected</button>';
741
549
  h += '<div class="bar-divider"></div>';
742
- h += '<button class="bar-btn bar-ghost" id="bar-clear">\\u2715</button>';
550
+ h += '<button class="bar-btn bar-ghost" id="bar-clear">' + IC.x + '</button>';
551
+ h += '</div>';
743
552
  h += '</div>';
744
553
 
745
554
  // Toast
746
555
  h += '<div class="gallery-toast" id="gallery-toast"></div>';
747
556
 
748
557
  root.innerHTML = h;
558
+ renderThemeToggle();
749
559
 
750
560
  // Re-apply selection state
751
561
  selected.forEach(function(i) {
@@ -779,37 +589,16 @@ function attachEvents() {
779
589
  _eventsAttached = true;
780
590
  var root = document.getElementById("app");
781
591
 
782
- root.addEventListener("input", function(e) {
783
- if (e.target && e.target.id === "filter-input") handleFilter();
784
- });
785
-
786
- document.addEventListener("click", function(e) {
787
- var dd = document.getElementById("aspect-dropdown");
788
- if (dd && !dd.contains(e.target)) {
789
- var menu = document.getElementById("aspect-menu");
790
- var btn = document.getElementById("aspect-btn");
791
- if (menu) menu.classList.remove("open");
792
- if (btn) btn.classList.remove("open");
793
- }
794
- });
795
-
796
592
  root.addEventListener("click", function(e) {
797
593
  var el = e.target;
798
594
  while (el && el !== root) {
799
595
  if (el.id === "load-more-btn") { loadMore(); return; }
800
596
  if (el.id === "refresh-gallery") { refreshGallery(); return; }
801
597
  if (el.id === "select-all-btn") { toggleSelectAll(); return; }
802
- if (el.id === "filter-clear") { clearFilter(); return; }
803
598
  if (el.id === "bar-copy-optimized") { copySelectedUrls("optimized"); return; }
804
599
  if (el.id === "bar-copy-original") { copySelectedUrls("original"); return; }
805
600
  if (el.id === "bar-download") { downloadSelected(); return; }
806
601
  if (el.id === "bar-clear") { clearSelection(); return; }
807
- if (el.id === "aspect-btn" || el.parentElement && el.parentElement.id === "aspect-btn") {
808
- toggleAspectMenu(e); return;
809
- }
810
- if (el.classList && el.classList.contains("aspect-option")) {
811
- selectAspect(el.getAttribute("data-value") || ""); return;
812
- }
813
602
  if (el.dataset && el.dataset.copyOriginal != null) {
814
603
  e.stopPropagation();
815
604
  copyAssetUrl("original", parseInt(el.dataset.copyOriginal, 10)); return;
@@ -970,6 +759,7 @@ function showFetchPrompt() {
970
759
  h += '<button class="prompt-btn prompt-btn-primary" id="fetch-direct-btn">Fetch Directly</button>';
971
760
  h += "</div></div>";
972
761
  root.innerHTML = h;
762
+ renderThemeToggle();
973
763
  document.getElementById("fetch-direct-btn").addEventListener("click", function() { fetchDirect(); });
974
764
  }
975
765
 
@@ -989,6 +779,9 @@ async function fetchDirect() {
989
779
  console.log(LOG_PREFIX, "fetchDirect ->", name);
990
780
 
991
781
  document.getElementById("app").innerHTML = '<div class="status">Fetching assets\\u2026</div>';
782
+ requestAnimationFrame(function() {
783
+ app.reportSize(Math.max(document.documentElement.scrollHeight, MIN_HEIGHT));
784
+ });
992
785
  try {
993
786
  var res = await app.callServerTool({ name: name, arguments: args });
994
787
  var data = ingestResult(res);
@@ -1040,6 +833,7 @@ async function loadMore() {
1040
833
  function refreshGallery() {
1041
834
  allResources = [];
1042
835
  lastCursor = null;
836
+ selected.clear();
1043
837
  fetchDirect();
1044
838
  }
1045
839
 
@@ -1047,7 +841,6 @@ function refreshGallery() {
1047
841
  document.addEventListener("keydown", function(e) {
1048
842
  if (e.key === "Escape") {
1049
843
  if (document.querySelector(".modal-overlay")) { closeModal(); return; }
1050
- if (filterQuery || aspectFilter) { clearFilter(); return; }
1051
844
  if (selected.size > 0) { clearSelection(); return; }
1052
845
  }
1053
846
  });
@@ -1078,6 +871,7 @@ ${GALLERY_CSS}
1078
871
  <div class="gallery-toast" id="gallery-toast"></div>
1079
872
 
1080
873
  <script>
874
+ ${SHARED_JS_ICONS}
1081
875
  ${SHARED_JS_MCP_CLIENT}
1082
876
  ${SHARED_JS_HELPERS}
1083
877
  ${SHARED_JS_TOOLTIPS}