@cloudinary/asset-management-mcp 0.9.0 → 0.9.2

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 (64) hide show
  1. package/README.md +12 -15
  2. package/bin/mcp-server.js +269 -379
  3. package/bin/mcp-server.js.map +17 -17
  4. package/esm/funcs/searchSearchAssets.d.ts +46 -2
  5. package/esm/funcs/searchSearchAssets.d.ts.map +1 -1
  6. package/esm/funcs/searchSearchAssets.js +46 -2
  7. package/esm/funcs/searchSearchAssets.js.map +1 -1
  8. package/esm/landing-page.d.ts.map +1 -1
  9. package/esm/landing-page.js +9 -3
  10. package/esm/landing-page.js.map +1 -1
  11. package/esm/lib/config.d.ts +4 -4
  12. package/esm/lib/config.js +4 -4
  13. package/esm/mcp-server/apps/app-shared.d.ts +5 -4
  14. package/esm/mcp-server/apps/app-shared.d.ts.map +1 -1
  15. package/esm/mcp-server/apps/app-shared.js +79 -12
  16. package/esm/mcp-server/apps/app-shared.js.map +1 -1
  17. package/esm/mcp-server/apps/asset-details-app.d.ts.map +1 -1
  18. package/esm/mcp-server/apps/asset-details-app.js +7 -14
  19. package/esm/mcp-server/apps/asset-details-app.js.map +1 -1
  20. package/esm/mcp-server/apps/asset-gallery-app.d.ts.map +1 -1
  21. package/esm/mcp-server/apps/asset-gallery-app.js +75 -298
  22. package/esm/mcp-server/apps/asset-gallery-app.js.map +1 -1
  23. package/esm/mcp-server/apps/asset-upload-app.d.ts.map +1 -1
  24. package/esm/mcp-server/apps/asset-upload-app.js +29 -24
  25. package/esm/mcp-server/apps/asset-upload-app.js.map +1 -1
  26. package/esm/mcp-server/apps/config.d.ts.map +1 -1
  27. package/esm/mcp-server/apps/config.js +1 -2
  28. package/esm/mcp-server/apps/config.js.map +1 -1
  29. package/esm/mcp-server/apps/extensions.d.ts.map +1 -1
  30. package/esm/mcp-server/apps/extensions.js +6 -1
  31. package/esm/mcp-server/apps/extensions.js.map +1 -1
  32. package/esm/mcp-server/cli/serve/impl.js +1 -1
  33. package/esm/mcp-server/cli/serve/impl.js.map +1 -1
  34. package/esm/mcp-server/mcp-server.js +1 -1
  35. package/esm/mcp-server/server.js +1 -1
  36. package/esm/mcp-server/tools/searchSearchAssets.d.ts.map +1 -1
  37. package/esm/mcp-server/tools/searchSearchAssets.js +46 -2
  38. package/esm/mcp-server/tools/searchSearchAssets.js.map +1 -1
  39. package/esm/models/searchparameters.d.ts +4 -1
  40. package/esm/models/searchparameters.d.ts.map +1 -1
  41. package/esm/models/searchparameters.js +9 -8
  42. package/esm/models/searchparameters.js.map +1 -1
  43. package/esm/tool-names.js +1 -1
  44. package/esm/tool-names.js.map +1 -1
  45. package/esm/types/bigint.d.ts.map +1 -1
  46. package/esm/types/bigint.js +4 -3
  47. package/esm/types/bigint.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/funcs/searchSearchAssets.ts +46 -2
  50. package/src/landing-page.ts +9 -3
  51. package/src/lib/config.ts +4 -4
  52. package/src/mcp-server/apps/app-shared.ts +80 -12
  53. package/src/mcp-server/apps/asset-details-app.ts +7 -13
  54. package/src/mcp-server/apps/asset-gallery-app.ts +75 -297
  55. package/src/mcp-server/apps/asset-upload-app.ts +29 -23
  56. package/src/mcp-server/apps/config.ts +1 -2
  57. package/src/mcp-server/apps/extensions.ts +6 -1
  58. package/src/mcp-server/cli/serve/impl.ts +1 -1
  59. package/src/mcp-server/mcp-server.ts +1 -1
  60. package/src/mcp-server/server.ts +1 -1
  61. package/src/mcp-server/tools/searchSearchAssets.ts +46 -2
  62. package/src/models/searchparameters.ts +25 -9
  63. package/src/tool-names.ts +1 -1
  64. 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;
@@ -265,7 +179,6 @@ const GALLERY_CSS = /* css */ `
265
179
  opacity: 0; pointer-events: none; z-index: 100; white-space: nowrap;
266
180
  }
267
181
  .select-bar.visible { transform: translateX(-50%) translateY(0); opacity: 1; pointer-events: all; }
268
- .select-bar-spacer { height: 72px; }
269
182
  .select-count { font-size: 13px; font-weight: 600; margin-right: 8px; }
270
183
  .bar-btn {
271
184
  height: 36px; padding: 0 14px; border: none; border-radius: 30px;
@@ -306,8 +219,6 @@ var pendingCall = {
306
219
  args: null,
307
220
  };
308
221
  var selected = new Set();
309
- var filterQuery = "";
310
- var aspectFilter = "";
311
222
  var app = new MCPApp({ name: "Cloudinary Asset Gallery", version: "1.0.0" });
312
223
  setupHostContext(app);
313
224
 
@@ -331,24 +242,6 @@ function showToast(msg) {
331
242
  _toastTimer = setTimeout(function() { t.classList.remove("show"); }, 2000);
332
243
  }
333
244
 
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
245
  function updateSelectBar() {
353
246
  var bar = document.getElementById("select-bar");
354
247
  var countEl = document.getElementById("select-count");
@@ -356,8 +249,6 @@ function updateSelectBar() {
356
249
  var n = selected.size;
357
250
  countEl.textContent = n + " selected";
358
251
  bar.classList.toggle("visible", n > 0);
359
- var spacer = document.getElementById("select-bar-spacer");
360
- if (spacer) spacer.style.display = n > 0 ? "" : "none";
361
252
  var btn = document.getElementById("select-all-btn");
362
253
  if (btn) {
363
254
  var visible = getVisibleIndices();
@@ -424,11 +315,9 @@ function copyAssetUrl(type, idx) {
424
315
  var url = r.secure_url || r.url || "";
425
316
  var copyUrl = type === "optimized" ? optimizedUrl(url, r) : url;
426
317
  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)); }
318
+ copyText(copyUrl).then(function() {
319
+ showToast(type === "optimized" ? "\\u2728 Optimized URL copied" : "URL copied");
320
+ }).catch(function(e) { showError("Copy Failed", e && e.message ? e.message : String(e)); });
432
321
  }
433
322
 
434
323
  function downloadOne(idx) {
@@ -450,134 +339,74 @@ function copySelectedUrls(type) {
450
339
  urls.push(type === "optimized" ? optimizedUrl(url, r) : url);
451
340
  });
452
341
  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)); }
342
+ copyText(urls.join("\\n")).then(function() {
343
+ showToast(urls.length + " " + (type === "optimized" ? "optimized " : "") + "URLs copied");
344
+ }).catch(function(e) { showError("Copy Failed", e && e.message ? e.message : String(e)); });
458
345
  }
459
346
 
460
- function downloadSelected() {
461
- var count = 0;
347
+ async function downloadSelected() {
348
+ if (selected.size === 0) return;
349
+
350
+ var picks = [];
462
351
  selected.forEach(function(i) {
463
352
  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++; }
353
+ if (r && r.public_id) picks.push(r);
468
354
  });
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() : "";
355
+ if (!picks.length) return;
356
+
357
+ var requestBody = {
358
+ mode: "create",
359
+ target_format: "zip",
360
+ keep_derived: true,
361
+ target_public_id: "mcp-gallery-archive-" + Date.now(),
362
+ fully_qualified_public_ids: picks.map(function(r) {
363
+ return (r.resource_type || "image") + "/" + (r.type || "upload") + "/" + r.public_id;
364
+ }),
365
+ };
366
+
367
+ var btn = document.getElementById("bar-download");
368
+ var origLabel = btn ? btn.innerHTML : "";
369
+ if (btn) { btn.innerHTML = "Creating archive\\u2026"; btn.disabled = true; }
475
370
 
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);
485
-
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;
371
+ try {
372
+ var res = await app.callServerTool({
373
+ name: "generate-archive",
374
+ arguments: {
375
+ resource_type: "all",
376
+ RequestBody: requestBody,
377
+ },
378
+ });
491
379
 
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>' : '');
380
+ var data = ingestResult(res);
381
+ if (data && (data._error || data._parseError)) {
382
+ showError("Archive Failed", unwrapApiError(data._message));
383
+ return;
521
384
  }
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);
385
+ var archiveUrl = data && (data.secure_url || data.url);
386
+ if (!archiveUrl) {
387
+ showError("Archive Failed", "No delivery URL returned.");
388
+ return;
540
389
  }
541
- if (noRes) noRes.textContent = "No results" + (filterQuery ? ' for "' + filterQuery + '"' : "") + (aspectFilter ? " in " + aspectFilter + " images" : "");
542
- } else if (noRes) {
543
- noRes.remove();
390
+ try { await copyText(archiveUrl); } catch (e) { /* ignore */ }
391
+ app._rpc("ui/open-link", { url: archiveUrl });
392
+ showToast("Archive saved as raw in Cloudinary \\u2014 opening URL (" + picks.length + " asset" + (picks.length > 1 ? "s" : "") + ")");
393
+ } catch (e) {
394
+ showError("Archive Failed", unwrapApiError(e && e.message ? e.message : String(e)));
395
+ } finally {
396
+ if (btn) { btn.innerHTML = origLabel; btn.disabled = false; }
544
397
  }
545
398
  }
546
399
 
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();
400
+ function unwrapApiError(raw) {
401
+ if (!raw) return "Unknown error.";
402
+ var msg = String(raw);
403
+ try {
404
+ if (msg.charAt(0) === "{") {
405
+ var parsed = JSON.parse(msg);
406
+ msg = (parsed && parsed.error && parsed.error.message) || msg;
407
+ }
408
+ } catch (e) { /* keep raw */ }
409
+ return msg;
581
410
  }
582
411
 
583
412
  function render() {
@@ -597,40 +426,11 @@ function render() {
597
426
  h += '<h1>Results</h1>';
598
427
  h += '<span class="count-badge" id="count-badge">' + allResources.length + (lastCursor ? "+" : "") + ' items</span>';
599
428
  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>';
603
- h += '</div>';
429
+ h += '<div id="header-actions" style="display:flex;align-items:center;gap:8px">';
430
+ h += '<button class="icon-btn" id="select-all-btn">Select all</button>';
431
+ h += '<button class="icon-btn icon-only" id="refresh-gallery" title="Refresh">' + IC.refresh + '</button>';
604
432
  h += '</div>';
605
-
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
433
  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
434
 
635
435
  // Grid
636
436
  h += '<div class="grid" id="gallery-grid">';
@@ -680,8 +480,8 @@ function render() {
680
480
  if (url) {
681
481
  h += '<div class="card-actions">';
682
482
  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>';
483
+ if (rt !== "raw") h += '<button class="action-btn act-optimized" data-copy-optimized="' + i + '">' + IC.zap + ' Optimized</button>';
484
+ h += '<button class="action-btn act-download" data-download="' + i + '" title="Download">' + IC.arrowDown + '</button>';
685
485
  h += '</div>';
686
486
  }
687
487
 
@@ -728,24 +528,22 @@ function render() {
728
528
  h += "</div>";
729
529
  }
730
530
 
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
531
  // Multi-select bar
735
532
  h += '<div class="select-bar" id="select-bar">';
736
533
  h += '<span class="select-count" id="select-count">0 selected</span>';
737
534
  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>';
535
+ h += '<button class="bar-btn bar-primary" id="bar-copy-optimized" style="display:none">' + IC.zap + ' Copy Optimized</button>';
739
536
  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>';
537
+ h += '<button class="bar-btn bar-secondary" id="bar-download">' + IC.arrowDown + ' Download Selected</button>';
741
538
  h += '<div class="bar-divider"></div>';
742
- h += '<button class="bar-btn bar-ghost" id="bar-clear">\\u2715</button>';
539
+ h += '<button class="bar-btn bar-ghost" id="bar-clear">' + IC.x + '</button>';
743
540
  h += '</div>';
744
541
 
745
542
  // Toast
746
543
  h += '<div class="gallery-toast" id="gallery-toast"></div>';
747
544
 
748
545
  root.innerHTML = h;
546
+ renderThemeToggle();
749
547
 
750
548
  // Re-apply selection state
751
549
  selected.forEach(function(i) {
@@ -779,37 +577,16 @@ function attachEvents() {
779
577
  _eventsAttached = true;
780
578
  var root = document.getElementById("app");
781
579
 
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
580
  root.addEventListener("click", function(e) {
797
581
  var el = e.target;
798
582
  while (el && el !== root) {
799
583
  if (el.id === "load-more-btn") { loadMore(); return; }
800
584
  if (el.id === "refresh-gallery") { refreshGallery(); return; }
801
585
  if (el.id === "select-all-btn") { toggleSelectAll(); return; }
802
- if (el.id === "filter-clear") { clearFilter(); return; }
803
586
  if (el.id === "bar-copy-optimized") { copySelectedUrls("optimized"); return; }
804
587
  if (el.id === "bar-copy-original") { copySelectedUrls("original"); return; }
805
588
  if (el.id === "bar-download") { downloadSelected(); return; }
806
589
  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
590
  if (el.dataset && el.dataset.copyOriginal != null) {
814
591
  e.stopPropagation();
815
592
  copyAssetUrl("original", parseInt(el.dataset.copyOriginal, 10)); return;
@@ -970,6 +747,7 @@ function showFetchPrompt() {
970
747
  h += '<button class="prompt-btn prompt-btn-primary" id="fetch-direct-btn">Fetch Directly</button>';
971
748
  h += "</div></div>";
972
749
  root.innerHTML = h;
750
+ renderThemeToggle();
973
751
  document.getElementById("fetch-direct-btn").addEventListener("click", function() { fetchDirect(); });
974
752
  }
975
753
 
@@ -1047,7 +825,6 @@ function refreshGallery() {
1047
825
  document.addEventListener("keydown", function(e) {
1048
826
  if (e.key === "Escape") {
1049
827
  if (document.querySelector(".modal-overlay")) { closeModal(); return; }
1050
- if (filterQuery || aspectFilter) { clearFilter(); return; }
1051
828
  if (selected.size > 0) { clearSelection(); return; }
1052
829
  }
1053
830
  });
@@ -1078,6 +855,7 @@ ${GALLERY_CSS}
1078
855
  <div class="gallery-toast" id="gallery-toast"></div>
1079
856
 
1080
857
  <script>
858
+ ${SHARED_JS_ICONS}
1081
859
  ${SHARED_JS_MCP_CLIENT}
1082
860
  ${SHARED_JS_HELPERS}
1083
861
  ${SHARED_JS_TOOLTIPS}