← 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
M public/panels/sounds.htmlM public/panels/sounds.js
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 & 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'] = {
({ '&': '&', '<': '<', '>': '>', '"': '"' }[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 →