[object Object]

← back to Marketing Command Center

Sounds panel: move assets into left panel with in-panel iframe preview, no link-out

0b8eb41f55e570b5d782cc3206e4624dae500a54 · 2026-08-12 12:01:59 -0700 · Steve

Files touched

Diff

commit 0b8eb41f55e570b5d782cc3206e4624dae500a54
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 12:01:59 2026 -0700

    Sounds panel: move assets into left panel with in-panel iframe preview, no link-out
---
 public/panels/sounds.html | 87 ++++++++++++++++++++++++++++++++++-------------
 public/panels/sounds.js   | 85 +++++++++++++++++++++++++++++++++++++++------
 2 files changed, 137 insertions(+), 35 deletions(-)

diff --git a/public/panels/sounds.html b/public/panels/sounds.html
index 9f046f7..01c5345 100644
--- a/public/panels/sounds.html
+++ b/public/panels/sounds.html
@@ -1,39 +1,78 @@
-<div class="muted-banner" id="snd-banner">Loading sounds & assets…</div>
+<div class="muted-banner" id="snd-banner">Loading sounds &amp; assets…</div>
+<div class="muted snd-legend" id="snd-legend" style="font-size:12px;margin:0 0 12px;line-height:1.6"></div>
 
-<div class="card" style="margin-bottom:14px">
-  <div class="row" style="gap:10px;align-items:center;flex-wrap:wrap">
-    <input type="search" id="snd-search" placeholder="Filter sources…" style="flex:1;min-width:200px" autocomplete="off">
-    <div id="snd-badgefilter" class="snd-badgefilter"></div>
-  </div>
-  <div class="muted" id="snd-legend" style="font-size:12px;margin-top:10px;line-height:1.6"></div>
-</div>
+<div class="snd-split">
+  <!-- LEFT: every asset lives in its own panel here -->
+  <aside class="snd-left">
+    <div class="snd-controls">
+      <input type="search" id="snd-search" placeholder="Filter sources…" autocomplete="off">
+      <div id="snd-badgefilter" class="snd-badgefilter"></div>
+    </div>
+    <div id="snd-sections" class="snd-list"><div class="muted">Loading…</div></div>
+  </aside>
 
-<div id="snd-sections"><div class="muted">Loading…</div></div>
+  <!-- RIGHT: in-panel preview, never links out -->
+  <section class="snd-view" id="snd-view">
+    <div class="snd-ph">Pick an asset on the left ← to preview it here.</div>
+  </section>
+</div>
 
 <style>
+.snd-split{display:grid;grid-template-columns:300px 1fr;gap:16px;align-items:start}
+@media(max-width:900px){.snd-split{grid-template-columns:1fr}}
+
+/* left panel */
+.snd-left{border:1px solid var(--line);border-radius:12px;background:#fff;
+ box-shadow:0 1px 3px rgba(0,0,0,.04);display:flex;flex-direction:column;
+ max-height:calc(100vh - 210px);overflow:hidden}
+.snd-controls{padding:12px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:10px}
+.snd-controls input[type=search]{width:100%;box-sizing:border-box}
 .snd-badgefilter{display:flex;gap:6px;flex-wrap:wrap}
 .snd-bf{background:transparent;border:1px solid var(--line);border-radius:999px;padding:5px 11px;cursor:pointer;
  font:600 12px/1 Inter;color:var(--ink)}
 .snd-bf:hover{background:var(--cream)}
 .snd-bf.on{background:var(--accent);color:#fff;border-color:var(--accent)}
 .snd-legend b{color:var(--ink)}
-.snd-sec{margin-bottom:20px}
-.snd-sechead{display:flex;align-items:baseline;gap:10px;margin:0 0 4px}
-.snd-sechead h2{margin:0;font:600 20px/1.1 "Cormorant Garamond",Georgia,serif}
-.snd-sechead .ct{font:700 11px/1 Inter;background:var(--cream);color:var(--accent);border-radius:999px;padding:3px 9px}
-.snd-secblurb{font-size:12.5px;color:var(--mut);margin:0 0 12px}
-.snd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
-.snd-card{border:1px solid var(--line);border-radius:12px;padding:14px;background:#fff;display:flex;flex-direction:column;gap:6px;
- text-decoration:none;color:inherit;transition:border-color .12s,transform .12s}
-.snd-card:hover{border-color:var(--accent);transform:translateY(-1px)}
-.snd-card .st{display:flex;align-items:center;gap:8px}
-.snd-card .nm{font:600 14.5px/1.2 Inter;flex:1;min-width:0}
-.snd-card .arr{color:var(--mut);font-size:13px}
-.snd-card .bdg{font:700 9.5px/1 Inter;border-radius:999px;padding:3px 8px;white-space:nowrap}
+
+.snd-list{overflow-y:auto;padding:8px;flex:1}
+.snd-sec{margin-bottom:14px}
+.snd-sechead{display:flex;align-items:baseline;gap:8px;margin:2px 4px 6px}
+.snd-sechead h2{margin:0;font:600 16px/1.1 "Cormorant Garamond",Georgia,serif}
+.snd-sechead .ct{font:700 10px/1 Inter;background:var(--cream);color:var(--accent);border-radius:999px;padding:3px 8px}
+
+/* an asset = a row, NOT a link — selecting previews it in-panel */
+.snd-item{width:100%;text-align:left;border:1px solid var(--line);border-radius:10px;padding:9px 11px;background:#fff;
+ display:flex;flex-direction:column;gap:4px;cursor:pointer;margin-bottom:6px;transition:border-color .12s,background .12s}
+.snd-item:hover{border-color:var(--accent);background:var(--paper)}
+.snd-item.on{border-color:var(--accent);background:var(--cream);box-shadow:inset 3px 0 0 var(--accent)}
+.snd-item .st{display:flex;align-items:center;gap:8px}
+.snd-item .nm{font:600 13.5px/1.2 Inter;flex:1;min-width:0}
+.snd-item .nt{font-size:11.5px;line-height:1.4;color:#5a544b;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
+.snd-item .bdg,.snd-vhead .bdg{font:700 9px/1 Inter;border-radius:999px;padding:3px 8px;white-space:nowrap}
 .bdg-brandsafe{background:#e3efe0;color:#2f6b34}
 .bdg-cc{background:#eaf0f7;color:#33578a}
 .bdg-listen{background:#fdf3dc;color:#8a6a1e}
 .bdg-tiktok{background:#f1e8f3;color:#7a3a8a}
-.snd-card .nt{font-size:12px;line-height:1.45;color:#5a544b}
-.snd-empty{color:var(--mut);font-size:12.5px;padding:8px 0}
+.snd-empty{color:var(--mut);font-size:12.5px;padding:8px 4px}
+
+/* right preview panel */
+.snd-view{border:1px solid var(--line);border-radius:12px;background:#fff;
+ box-shadow:0 1px 3px rgba(0,0,0,.04);min-height:calc(100vh - 210px);display:flex;flex-direction:column;overflow:hidden}
+.snd-ph{margin:auto;color:var(--mut);font-size:13px;padding:40px}
+.snd-vhead{padding:14px 16px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:6px}
+.snd-vhead .top{display:flex;align-items:center;gap:10px}
+.snd-vhead h2{margin:0;font:600 20px/1.15 "Cormorant Garamond",Georgia,serif;flex:1;min-width:0}
+.snd-vhead .sec{font-size:11.5px;color:var(--mut)}
+.snd-vhead .nt{font-size:12.5px;line-height:1.5;color:#5a544b;margin:2px 0 0}
+.snd-urlrow{display:flex;gap:8px;align-items:center;margin-top:4px}
+.snd-url{flex:1;min-width:0;font:500 11.5px/1.3 ui-monospace,Menlo,monospace;color:var(--mut);
+ background:var(--paper);border:1px solid var(--line);border-radius:8px;padding:7px 9px;
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.snd-copy{flex:none;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer;
+ font:600 11.5px/1 Inter;color:var(--ink);padding:8px 11px}
+.snd-copy:hover{background:var(--cream)}
+.snd-frame-wrap{flex:1;position:relative;background:var(--paper)}
+.snd-frame-wrap iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
+.snd-blockmsg{position:absolute;left:0;right:0;bottom:0;font-size:11.5px;color:var(--mut);
+ background:rgba(251,249,245,.94);border-top:1px solid var(--line);padding:8px 12px;text-align:center}
 </style>
diff --git a/public/panels/sounds.js b/public/panels/sounds.js
index c04b9d2..4a29e6b 100644
--- a/public/panels/sounds.js
+++ b/public/panels/sounds.js
@@ -1,6 +1,9 @@
 // Sounds & Assets panel — free music + TikTok creative sources, badged by
-// brand-safety. Search + badge facets. Pure link directory (data from
-// /api/sounds/catalog); every card opens the source in a new tab.
+// brand-safety. Two-column master-detail: every asset lives in its own LEFT
+// panel (search + badge facets + grouped list); selecting one PREVIEWS it in
+// an in-panel iframe on the right. Deliberately does NOT link out — no
+// target="_blank" anchors; the source URL is shown as copyable text only.
+// Data from /api/sounds/catalog.
 window.MCC_PANELS = window.MCC_PANELS || {};
 window.MCC_PANELS['sounds'] = {
   init(root) {
@@ -9,7 +12,10 @@ window.MCC_PANELS['sounds'] = {
       ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
     const $ = sel => root.querySelector(sel);
 
-    const state = { catalog: [], badges: {}, q: '', badge: 'all' };
+    const state = { catalog: [], badges: {}, q: '', badge: 'all', selKey: null };
+
+    // stable key for an item so selection survives re-renders (filtering)
+    const keyOf = it => (it.name || '') + '' + (it.url || '');
 
     async function load() {
       let d;
@@ -19,7 +25,7 @@ window.MCC_PANELS['sounds'] = {
       state.badges = d.badges || {};
       const total = state.catalog.reduce((n, s) => n + s.items.length, 0);
       $('#snd-banner').innerHTML = `<b>${total}</b> free / brand-safe sources across ${state.catalog.length} groups. ` +
-        `Filter by type, then click through — sources open in a new tab.`;
+        `Filter on the left, then click an asset to preview it here — nothing opens a new tab.`;
       renderLegend();
       renderBadgeFilter();
       renderSections();
@@ -59,27 +65,84 @@ window.MCC_PANELS['sounds'] = {
         shown += items.length;
         html += `<div class="snd-sec">
           <div class="snd-sechead"><h2>${esc(sec.title)}</h2><span class="ct">${items.length}</span></div>
-          <div class="snd-secblurb">${esc(sec.blurb || '')}</div>
-          <div class="snd-grid">${items.map(cardHTML).join('')}</div>
+          ${items.map(it => itemHTML(it, sec.title)).join('')}
         </div>`;
       }
       box.innerHTML = shown ? html : '<div class="snd-empty">No sources match that filter.</div>';
+      box.querySelectorAll('.snd-item').forEach(el => el.onclick = () => {
+        const k = el.getAttribute('data-k');
+        const found = findByKey(k);
+        if (found) select(found.item, found.section);
+      });
+    }
+
+    function findByKey(k) {
+      for (const sec of state.catalog) {
+        for (const it of sec.items) if (keyOf(it) === k) return { item: it, section: sec.title };
+      }
+      return null;
     }
 
-    function cardHTML(it) {
+    function itemHTML(it, secTitle) {
       const b = state.badges[it.badge] || { label: it.badge };
-      return `<a class="snd-card" href="${esc(it.url)}" target="_blank" rel="noopener noreferrer">
+      const k = keyOf(it);
+      const on = state.selKey === k ? ' on' : '';
+      return `<div class="snd-item${on}" data-k="${esc(k)}" role="button" tabindex="0">
         <div class="st">
           <span class="nm">${esc(it.name)}</span>
           <span class="bdg bdg-${esc(it.badge)}">${esc(b.label)}</span>
         </div>
-        <div class="nt">${esc(it.note || '')}</div>
-        <div class="arr">Open ↗</div>
-      </a>`;
+        ${it.note ? `<div class="nt">${esc(it.note)}</div>` : ''}
+      </div>`;
+    }
+
+    // Preview the asset IN-PANEL. No navigation, no new tab — the source loads
+    // in an iframe; if it refuses to embed (X-Frame-Options), the header card
+    // + copyable URL keep the panel useful.
+    function select(it, secTitle) {
+      state.selKey = keyOf(it);
+      // repaint left rows so the active one highlights
+      root.querySelectorAll('.snd-item').forEach(el =>
+        el.classList.toggle('on', el.getAttribute('data-k') === state.selKey));
+
+      const b = state.badges[it.badge] || { label: it.badge };
+      const view = $('#snd-view');
+      view.innerHTML = `
+        <div class="snd-vhead">
+          <div class="top">
+            <h2>${esc(it.name)}</h2>
+            <span class="bdg bdg-${esc(it.badge)}">${esc(b.label)}</span>
+          </div>
+          <div class="sec">${esc(secTitle || '')}</div>
+          ${it.note ? `<div class="nt">${esc(it.note)}</div>` : ''}
+          <div class="snd-urlrow">
+            <div class="snd-url" title="${esc(it.url)}">${esc(it.url)}</div>
+            <button type="button" class="snd-copy" data-url="${esc(it.url)}">Copy URL</button>
+          </div>
+        </div>
+        <div class="snd-frame-wrap">
+          <iframe src="${esc(it.url)}" title="${esc(it.name)} preview"
+                  loading="lazy" referrerpolicy="no-referrer"></iframe>
+          <div class="snd-blockmsg">Preview loads in-panel. If it stays blank, this source blocks embedding — use Copy URL.</div>
+        </div>`;
+
+      const copy = view.querySelector('.snd-copy');
+      copy.onclick = async () => {
+        try { await navigator.clipboard.writeText(copy.getAttribute('data-url')); copy.textContent = 'Copied ✓'; }
+        catch (_) { copy.textContent = 'Copy failed'; }
+        setTimeout(() => (copy.textContent = 'Copy URL'), 1400);
+      };
     }
 
     $('#snd-search').addEventListener('input', e => { state.q = e.target.value; renderSections(); });
 
+    // keyboard: Enter/Space selects the focused asset row
+    $('#snd-sections').addEventListener('keydown', e => {
+      if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('snd-item')) {
+        e.preventDefault(); e.target.click();
+      }
+    });
+
     load();
   },
 };

← 12f7c09 auto-data-snapshot: 2026-08-12T11:28:02 (1 data files) — pub  ·  back to Marketing Command Center  ·  chore: refactor (drop unused param), v1.8.2 (session close) 488e005 →