[object Object]

← back to Marketing Command Center

Add separate Showroom picker area to Make-a-post so showroom-only lines (PJ) stay out of the general pool with correct language

da0f1f854497015b422304dc491b1f64b8a8ed76 · 2026-09-03 11:19:28 -0700 · Steve

Backend (modules/assets/index.js): read the canonical showroom-vendors.json at
runtime (60s memo, default [Phillip Jeffries]); carry vendor + a showroom flag
through both catalog maps; add GET /api/assets/showroom that surfaces showroom
lines via on-site search (they are absent from browse by design); include the
canonical vendor list in catalog responses. Keyed off the list, never a
hardcoded name.

Frontend (quickpost.html/js): new 'Showroom' filter chip; showroom assets are
EXCLUDED from All ideas / DW picks and shown ONLY under Showroom; each showroom
card carries a Showroom pill; a persistent correct-language reminder banner
(invite to the DW showroom / book a visit -- never shop now / buy online) shows
when the Showroom area is active or a showroom picture is selected, and again in
the review step. Grid/List/Details + density + click-to-select preserved.

Verified headless: 18/18 checks (no PJ under All ideas/DW picks; Showroom chip
shows badged PJ cards + banner; 3 views + density; selection; zero console errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GDFjK9hKdrY7ExEupvWGr

Files touched

Diff

commit da0f1f854497015b422304dc491b1f64b8a8ed76
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 3 11:19:28 2026 -0700

    Add separate Showroom picker area to Make-a-post so showroom-only lines (PJ) stay out of the general pool with correct language
    
    Backend (modules/assets/index.js): read the canonical showroom-vendors.json at
    runtime (60s memo, default [Phillip Jeffries]); carry vendor + a showroom flag
    through both catalog maps; add GET /api/assets/showroom that surfaces showroom
    lines via on-site search (they are absent from browse by design); include the
    canonical vendor list in catalog responses. Keyed off the list, never a
    hardcoded name.
    
    Frontend (quickpost.html/js): new 'Showroom' filter chip; showroom assets are
    EXCLUDED from All ideas / DW picks and shown ONLY under Showroom; each showroom
    card carries a Showroom pill; a persistent correct-language reminder banner
    (invite to the DW showroom / book a visit -- never shop now / buy online) shows
    when the Showroom area is active or a showroom picture is selected, and again in
    the review step. Grid/List/Details + density + click-to-select preserved.
    
    Verified headless: 18/18 checks (no PJ under All ideas/DW picks; Showroom chip
    shows badged PJ cards + banner; 3 views + density; selection; zero console errors).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_018GDFjK9hKdrY7ExEupvWGr
---
 modules/assets/index.js      | 66 ++++++++++++++++++++++++++++++++++---
 public/panels/quickpost.html | 16 +++++++--
 public/panels/quickpost.js   | 78 ++++++++++++++++++++++++++++++++++++--------
 3 files changed, 139 insertions(+), 21 deletions(-)

diff --git a/modules/assets/index.js b/modules/assets/index.js
index 667a717..329c8eb 100644
--- a/modules/assets/index.js
+++ b/modules/assets/index.js
@@ -18,6 +18,7 @@
 // Both are gitignored runtime state — the library lives wherever the server runs
 // and survives code deploys (deploy ships code, not data).
 const fs = require('fs');
+const os = require('os');
 const path = require('path');
 const express = require('express');
 const { execFileSync } = require('child_process');
@@ -40,6 +41,32 @@ const CAT_TTL_MS = 6 * 60 * 60 * 1000; // 6h
 const DW_PRODUCTS_JSON = 'https://designerwallcoverings.com/products.json';
 const DW_SUGGEST = 'https://designerwallcoverings.com/search/suggest.json';
 
+// ── showroom-only lines (Steve's "addressable but not discoverable" rule) ─────
+// Canonical list of showroom-only vendors — the SINGLE source of truth. These
+// lines (e.g. Phillip Jeffries) stay reachable by direct URL + on-site search
+// but are HIDDEN from every browse/push surface. Here we mark each catalog asset
+// with a `showroom` flag so the "Make a post" picker can keep them OUT of the
+// general image pool and only surface them in a dedicated, correctly-framed
+// Showroom area. Keyed off the LIST, never a hardcoded vendor name. Re-read at
+// runtime (60s memo) so adding a vendor to the JSON flows without a code change.
+const SHOWROOM_VENDORS_FILE = process.env.SHOWROOM_VENDORS_FILE
+  || path.join(os.homedir(), 'Projects', 'fix-live-board', 'config', 'showroom-vendors.json');
+let _svCache = { at: 0, list: null };
+function showroomVendors() {
+  if (_svCache.list && (Date.now() - _svCache.at) < 60 * 1000) return _svCache.list;
+  let list = ['Phillip Jeffries']; // safe default if the file is unreadable
+  try {
+    const a = JSON.parse(fs.readFileSync(SHOWROOM_VENDORS_FILE, 'utf8'));
+    if (Array.isArray(a) && a.length) list = a.map(v => String(v).trim()).filter(Boolean);
+  } catch { /* keep default */ }
+  _svCache = { at: Date.now(), list };
+  return list;
+}
+function isShowroom(vendor) {
+  const v = String(vendor || '').trim().toLowerCase();
+  return !!v && showroomVendors().some(s => s.toLowerCase() === v);
+}
+
 const MIME_EXT = {
   'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/gif': 'gif',
   'image/webp': 'webp', 'image/svg+xml': 'svg', 'image/avif': 'avif',
@@ -245,7 +272,8 @@ async function suggestSearch(q, limit) {
   const ps = (d.resources && d.resources.results && d.resources.results.products) || [];
   return ps.map(p => {
     const img = (p.featured_image && (p.featured_image.url || p.featured_image)) || p.image || '';
-    return { title: p.title || p.handle || 'Untitled', handle: p.handle || '', type: p.type || '', url: normImg(img) };
+    const vendor = p.vendor || '';
+    return { title: p.title || p.handle || 'Untitled', handle: p.handle || '', type: p.type || '', vendor, showroom: isShowroom(vendor), url: normImg(img) };
   }).filter(p => p.url);
 }
 
@@ -274,10 +302,13 @@ async function fetchCatalog(opts) {
     for (const p of prods) {
       const img = (p.images || [])[0];
       if (!img || !img.src) continue;
+      const vendor = p.vendor || '';
       out.push({
         title: p.title || p.handle || 'Untitled',
         handle: p.handle || '',
         type: p.product_type || '',
+        vendor,
+        showroom: isShowroom(vendor),
         url: normImg(img.src),
       });
     }
@@ -444,18 +475,19 @@ module.exports = {
       const q = (req.query.q || '').toString().trim();
       const limit = Math.max(1, Math.min(200, parseInt(req.query.limit, 10) || 60));
       try {
+        const sv = showroomVendors();
         if (q) {
           try {
             const hits = await suggestSearch(q, limit);
-            if (hits.length) return res.json({ products: hits.slice(0, limit), total: hits.length, cached: false, source: 'search', fetchedAt: readCatCache()?.fetchedAt || null });
+            if (hits.length) return res.json({ products: hits.slice(0, limit), total: hits.length, cached: false, source: 'search', fetchedAt: readCatCache()?.fetchedAt || null, showroomVendors: sv });
           } catch { /* rate-limited / down → fall back to cached browse filter below */ }
           const { products, fetchedAt } = await fetchCatalog();
           const ql = q.toLowerCase();
-          const rows = products.filter(p => (p.title + ' ' + p.type + ' ' + p.handle).toLowerCase().includes(ql));
-          return res.json({ products: rows.slice(0, limit), total: rows.length, cached: true, source: 'browse-filter', fetchedAt: fetchedAt || null });
+          const rows = products.filter(p => (p.title + ' ' + p.type + ' ' + p.handle + ' ' + (p.vendor || '')).toLowerCase().includes(ql));
+          return res.json({ products: rows.slice(0, limit), total: rows.length, cached: true, source: 'browse-filter', fetchedAt: fetchedAt || null, showroomVendors: sv });
         }
         const { products, cached, fetchedAt } = await fetchCatalog();
-        res.json({ products: products.slice(0, limit), total: products.length, cached, source: 'browse', fetchedAt: fetchedAt || null });
+        res.json({ products: products.slice(0, limit), total: products.length, cached, source: 'browse', fetchedAt: fetchedAt || null, showroomVendors: sv });
       } catch (e) {
         res.status(502).json({ error: 'catalog fetch failed: ' + e.message });
       }
@@ -472,6 +504,30 @@ module.exports = {
       }
     });
 
+    // Showroom-only lines for the "Make a post" picker's dedicated Showroom
+    // area. Showroom vendors (e.g. Phillip Jeffries) are ABSENT from the browse
+    // pages by design ("addressable but not discoverable"), so we surface them
+    // here via on-site search — exactly the reachable-by-search channel the rule
+    // preserves. Every returned product is guaranteed showroom:true (near-match
+    // noise is dropped), and the canonical vendor list rides along so the client
+    // can frame the correct-language reminder without hardcoding a vendor name.
+    router.get('/showroom', async (_req, res) => {
+      const vendors = showroomVendors();
+      const out = [];
+      const seen = new Set();
+      for (const v of vendors) {
+        let hits = [];
+        try { hits = await suggestSearch(v, 10); } catch { /* one vendor down → keep going */ }
+        for (const h of hits) {
+          if (!h.showroom) continue;            // drop any non-showroom near-match
+          if (!h.url || seen.has(h.url)) continue;
+          seen.add(h.url);
+          out.push(h);
+        }
+      }
+      res.json({ products: out, total: out.length, source: 'showroom-search', showroomVendors: vendors });
+    });
+
     // Persist a catalog image into the library. Body: { name, url }.
     router.post('/save-catalog', (req, res) => {
       const { name, url } = req.body || {};
diff --git a/public/panels/quickpost.html b/public/panels/quickpost.html
index f1bb9c0..87977a3 100644
--- a/public/panels/quickpost.html
+++ b/public/panels/quickpost.html
@@ -9,11 +9,12 @@
   <main class="kp-card">
     <section class="kp-pane on" data-step="1">
       <div class="kp-number">1</div><div class="kp-copy"><h3>Pick a picture or video</h3><p>Tap one you like. Every DW post needs a picture or video.</p></div>
-      <div class="kp-asset-tools"><div class="kp-filter" role="group" aria-label="Picture choices"><button type="button" class="on" data-filter="all">✨ All ideas</button><button type="button" data-filter="catalog">🏷️ DW picks</button><button type="button" data-filter="upload">📤 My uploads</button></div><input id="kp-asset-search" type="search" placeholder="🔎 Find a color or room" aria-label="Find a picture"></div>
+      <div class="kp-asset-tools"><div class="kp-filter" role="group" aria-label="Picture choices"><button type="button" class="on" data-filter="all">✨ All ideas</button><button type="button" data-filter="catalog">🏷️ DW picks</button><button type="button" data-filter="upload">📤 My uploads</button><button type="button" class="kp-filter-showroom" data-filter="showroom">🏛️ Showroom</button></div><input id="kp-asset-search" type="search" placeholder="🔎 Find a color or room" aria-label="Find a picture"></div>
       <div class="kp-view-tools" role="group" aria-label="How the pictures are shown">
         <div class="kp-views" role="group" aria-label="View"><button type="button" class="on" data-view="grid" aria-pressed="true">▦ Grid</button><button type="button" data-view="list" aria-pressed="false">☰ List</button><button type="button" data-view="kv" aria-pressed="false">⊞ Details</button></div>
         <label class="kp-density"><span>Bigger</span><input id="kp-density" type="range" min="2" max="8" step="1" value="4" aria-label="Space between pictures — left is bigger, right fits more"><span>Smaller</span></label>
       </div>
+      <div id="kp-showroom-note" class="kp-showroom-note" role="note" hidden></div>
       <div id="kp-assets" class="kp-assets view-grid" aria-live="polite"><div class="kp-wait">Loading your pictures…</div></div>
       <details class="kp-more"><summary>Paste a picture link instead</summary><input id="kp-media" type="url" placeholder="https://…/picture.jpg" autocomplete="off"></details>
     </section>
@@ -64,4 +65,15 @@
 .view-kv .kp-asset .kp-thumb{width:70px;height:70px}
 .view-list .kp-asset-name,.view-kv .kp-asset-name{display:none}
 .view-list .kp-asset.on,.view-kv .kp-asset.on{border-color:var(--green);background:#ecf8f1}
-.view-list .kp-asset.on:after,.view-kv .kp-asset.on:after{top:50%;right:12px;transform:translateY(-50%)}</style>
+.view-list .kp-asset.on:after,.view-kv .kp-asset.on:after{top:50%;right:12px;transform:translateY(-50%)}
+/* Showroom-only lines — separate area, always the correct (invite-to-showroom) language */
+.kp-filter-showroom{border-color:#c9bdf2 !important;color:#5a45b8}
+.kp-filter-showroom.on{background:#6b4bd6 !important;border-color:#6b4bd6 !important;color:#fff !important}
+.kp-badge-showroom{position:absolute;left:7px;top:7px;z-index:2;background:#6b4bd6;color:#fff;font:900 9px/1 inherit;letter-spacing:.07em;text-transform:uppercase;padding:4px 7px;border-radius:999px;pointer-events:none;box-shadow:0 2px 6px rgba(23,34,53,.25)}
+.kp-asset.is-showroom{border-color:#c9bdf2}
+.view-list .kp-badge-showroom,.view-kv .kp-badge-showroom{position:static;display:inline-block;margin-top:6px;box-shadow:none}
+.kp-showroom-note{margin-top:14px;border:2px solid #d7ccf5;background:#f4f0fe;border-radius:16px;padding:13px 15px;color:#3c2f66;line-height:1.5}
+.kp-showroom-note[hidden]{display:none}
+.kp-showroom-note b{display:block;font-size:14px;color:#5a45b8;margin-bottom:3px}
+.kp-showroom-note em{font-style:normal;background:#e6dcff;border-radius:6px;padding:1px 5px;font-weight:700}
+.kp-showroom-note .no{color:#a33c2f;font-weight:800;text-decoration:line-through}</style>
diff --git a/public/panels/quickpost.js b/public/panels/quickpost.js
index fedede0..c6b8bbf 100644
--- a/public/panels/quickpost.js
+++ b/public/panels/quickpost.js
@@ -8,7 +8,7 @@ window.MCC_PANELS.quickpost = {
     const $$ = s => [...root.querySelectorAll(s)];
     const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
     const icons = {facebook:'📘',instagram:'📸',bluesky:'🦋',linkedin:'💼',tiktok:'🎵',youtube:'▶️',threads:'🧵'};
-    const state = { step:1, mediaUrl:'', song:null, channels:[], targets:[], status:{}, view:'grid', cols:4 };
+    const state = { step:1, mediaUrl:'', song:null, channels:[], targets:[], status:{}, view:'grid', cols:4, showroomVendors:[], showroomSelected:false };
     let allAssets = [];
     const PICKER_KEY = 'mcc.quickpost.picker';
     try { const s = JSON.parse(localStorage.getItem(PICKER_KEY) || '{}'); if (['grid','list','kv'].includes(s.view)) state.view = s.view; if (+s.cols >= 2 && +s.cols <= 8) state.cols = +s.cols; } catch (_) {}
@@ -47,42 +47,91 @@ window.MCC_PANELS.quickpost = {
 
     async function loadAssets() {
       try {
-        const [saved, catalog] = await Promise.all([
+        // Browse pool (general pictures) + saved library + the SEPARATE showroom
+        // area. Showroom-only lines (e.g. Phillip Jeffries) are absent from the
+        // browse pages by design, so we pull them via the dedicated endpoint —
+        // the only place they surface, and always with the correct language.
+        const [saved, catalog, showroom] = await Promise.all([
           fetch(O + '/api/assets/list', {credentials:'same-origin'}).then(r => r.json()),
-          fetch(O + '/api/assets/catalog?limit=200', {credentials:'same-origin'}).then(r => r.ok ? r.json() : {products:[]}).catch(() => ({products:[]}))
+          fetch(O + '/api/assets/catalog?limit=200', {credentials:'same-origin'}).then(r => r.ok ? r.json() : {products:[]}).catch(() => ({products:[]})),
+          fetch(O + '/api/assets/showroom', {credentials:'same-origin'}).then(r => r.ok ? r.json() : {products:[]}).catch(() => ({products:[]}))
         ]);
-        const savedAssets = (saved.assets || []).filter(a => a.src);
+        // Canonical showroom vendor list rides along with either catalog call —
+        // key off the LIST, never a hardcoded vendor name.
+        state.showroomVendors = catalog.showroomVendors || showroom.showroomVendors || ['Phillip Jeffries'];
+        const svLower = state.showroomVendors.map(s => String(s).toLowerCase());
+        // Saved assets carry no vendor field → infer showroom from the name only.
+        const savedAssets = (saved.assets || []).filter(a => a.src).map(a => {
+          const hay = `${a.name || ''} ${(a.tags || []).join(' ')}`.toLowerCase();
+          return Object.assign({}, a, { showroom: !!a.showroom || svLower.some(s => s && hay.includes(s)) });
+        });
         const catalogAssets = (catalog.products || []).filter(p => p.url).map(p => ({
           id: `catalog-${p.handle || p.url}`, name: p.title || 'DW catalog image', kind: 'catalog', src: p.url,
-          tags: ['dw-catalog', p.type || 'wallcovering'], aiTags: {}
+          tags: ['dw-catalog', p.type || 'wallcovering'], aiTags: {}, vendor: p.vendor || '', showroom: !!p.showroom
+        }));
+        const showroomAssets = (showroom.products || []).filter(p => p.url).map(p => ({
+          id: `showroom-${p.handle || p.url}`, name: p.title || 'Showroom line', kind: 'catalog', src: p.url,
+          tags: ['dw-catalog', p.vendor || '', p.type || ''].filter(Boolean), aiTags: {}, vendor: p.vendor || '', showroom: true
         }));
         const seen = new Set();
-        allAssets = [...savedAssets, ...catalogAssets].filter(a => { const key = a.src; if (seen.has(key)) return false; seen.add(key); return true; });
+        allAssets = [...savedAssets, ...showroomAssets, ...catalogAssets].filter(a => { const key = a.src; if (seen.has(key)) return false; seen.add(key); return true; });
         renderAssets();
       } catch (_) { $('#kp-assets').innerHTML = '<div class="kp-error">Pictures could not load. Paste a picture link below.</div>'; }
     }
     function cardInner(a, i) {
       const name = esc(a.name || 'DW picture');
       const img = `<img loading="lazy" src="${esc(a.src)}" alt="${esc(a.name || 'Wallpaper picture')}">`;
-      if (state.view === 'grid') return `${img}<span class="kp-asset-name">${name}</span>`;
-      const kind = { catalog:'DW pick', upload:'My upload' }[String(a.kind || '').toLowerCase()] || esc(a.kind || 'Picture');
+      const badge = a.showroom ? '<span class="kp-badge-showroom">🏛️ Showroom</span>' : '';
+      if (state.view === 'grid') return `${badge}${img}<span class="kp-asset-name">${name}</span>`;
+      const kind = a.showroom ? 'Showroom line' : ({ catalog:'DW pick', upload:'My upload' }[String(a.kind || '').toLowerCase()] || esc(a.kind || 'Picture'));
       const chips = state.view === 'kv'
         ? `<span class="kp-kv"><span class="kp-kv-chip"><i>Kind</i>${kind}</span>${(a.tags && a.tags.length) ? `<span class="kp-kv-chip"><i>Tags</i>${esc(a.tags.slice(0,3).join(' · '))}</span>` : ''}</span>`
         : '';
-      return `<span class="kp-thumb">${img}</span><span class="kp-row-main"><b class="kp-row-name">${name}</b>${chips}</span>`;
+      return `<span class="kp-thumb">${img}</span><span class="kp-row-main"><b class="kp-row-name">${name}</b>${chips}${a.showroom ? badge : ''}</span>`;
     }
     function renderAssets() {
       const filter = root.querySelector('.kp-filter button.on')?.dataset.filter || 'all';
       const q = ($('#kp-asset-search')?.value || '').trim().toLowerCase();
-      const assets = allAssets.filter(a => (filter === 'all' || String(a.kind || '').toLowerCase() === filter) && (!q || `${a.name || ''} ${(a.tags || []).join(' ')} ${JSON.stringify(a.aiTags || {})}`.toLowerCase().includes(q))).slice(0, 120);
+      const matchesQ = a => !q || `${a.name || ''} ${(a.tags || []).join(' ')} ${a.vendor || ''} ${JSON.stringify(a.aiTags || {})}`.toLowerCase().includes(q);
+      const assets = allAssets.filter(a => {
+        if (!matchesQ(a)) return false;
+        // Showroom-only lines appear ONLY in the dedicated Showroom area — never
+        // in "All ideas" or "DW picks" (addressable-but-not-discoverable).
+        if (filter === 'showroom') return !!a.showroom;
+        if (a.showroom) return false;
+        if (filter === 'all') return true;
+        return String(a.kind || '').toLowerCase() === filter;
+      }).slice(0, 120);
       const box = $('#kp-assets');
       box.classList.remove('view-grid','view-list','view-kv'); box.classList.add('view-' + state.view);
       box.style.setProperty('--cols', state.cols);
-      box.innerHTML = assets.length ? assets.map((a,i) => `<button class="kp-asset" type="button" data-url="${esc(new URL(a.src, O).href)}" aria-label="Choose ${esc(a.name || 'picture '+(i+1))}" aria-pressed="false">${cardInner(a,i)}</button>`).join('') : '<div class="kp-wait">No matching pictures yet. Try another word or paste a link below.</div>';
+      const emptyMsg = filter === 'showroom'
+        ? 'No showroom pictures yet. Showroom lines stay reachable by direct link and on-site search.'
+        : 'No matching pictures yet. Try another word or paste a link below.';
+      box.innerHTML = assets.length ? assets.map((a,i) => `<button class="kp-asset${a.showroom ? ' is-showroom' : ''}" type="button" data-url="${esc(new URL(a.src, O).href)}" data-showroom="${a.showroom ? '1' : '0'}" aria-label="Choose ${esc(a.name || 'picture '+(i+1))}${a.showroom ? ' (showroom line)' : ''}" aria-pressed="false">${cardInner(a,i)}</button>`).join('') : `<div class="kp-wait">${emptyMsg}</div>`;
       $$('.kp-asset').forEach(b => {
         if (b.dataset.url === state.mediaUrl) { b.classList.add('on'); b.setAttribute('aria-pressed','true'); }
-        b.onclick = () => { $$('.kp-asset').forEach(x => { x.classList.remove('on'); x.setAttribute('aria-pressed','false'); }); b.classList.add('on'); b.setAttribute('aria-pressed','true'); state.mediaUrl = b.dataset.url; $('#kp-media').value = state.mediaUrl; };
+        b.onclick = () => { $$('.kp-asset').forEach(x => { x.classList.remove('on'); x.setAttribute('aria-pressed','false'); }); b.classList.add('on'); b.setAttribute('aria-pressed','true'); state.mediaUrl = b.dataset.url; state.showroomSelected = b.dataset.showroom === '1'; $('#kp-media').value = state.mediaUrl; updateShowroomNote(); };
       });
+      updateShowroomNote();
+    }
+
+    // Persistent correct-language reminder. Shows whenever the Showroom area is
+    // active OR a showroom picture is selected. Framing per the doctrine:
+    // showroom lines may be featured on social/email ONLY to drive people to the
+    // physical showroom — invite the visit, never "shop now / buy online".
+    function updateShowroomNote() {
+      const el = $('#kp-showroom-note'); if (!el) return;
+      const filter = root.querySelector('.kp-filter button.on')?.dataset.filter || 'all';
+      const active = filter === 'showroom' || state.showroomSelected;
+      const vendors = state.showroomVendors && state.showroomVendors.length ? state.showroomVendors : ['Phillip Jeffries'];
+      const list = vendors.length === 1 ? esc(vendors[0])
+        : esc(vendors.slice(0, -1).join(', ')) + ' and ' + esc(vendors[vendors.length - 1]);
+      el.hidden = !active;
+      if (active) el.innerHTML = `<b>🏛️ Showroom-only line — invite people IN, don’t sell online</b>`
+        + `${list} is shown by appointment in our showroom, not on the online store. Feature it to bring people to us — `
+        + `<em>“See it in person at the Designer Wallcoverings showroom”</em> or <em>“Book a showroom visit to feel the samples.”</em> `
+        + `Please don’t write <span class="no">Shop now</span> or <span class="no">Buy online</span> — this line isn’t sold on the site.`;
     }
     function syncViewUI() {
       $$('.kp-views button').forEach(b => { const on = b.dataset.view === state.view; b.classList.toggle('on', on); b.setAttribute('aria-pressed', String(on)); });
@@ -94,7 +143,7 @@ window.MCC_PANELS.quickpost = {
     syncViewUI();
     $$('.kp-filter button').forEach(b => b.onclick = () => { $$('.kp-filter button').forEach(x => x.classList.remove('on')); b.classList.add('on'); renderAssets(); });
     $('#kp-asset-search').addEventListener('input', renderAssets);
-    $('#kp-media').addEventListener('input', e => { state.mediaUrl = e.target.value.trim(); $$('.kp-asset').forEach(x => x.classList.toggle('on', x.dataset.url === state.mediaUrl)); });
+    $('#kp-media').addEventListener('input', e => { state.mediaUrl = e.target.value.trim(); let sel = null; $$('.kp-asset').forEach(x => { const on = x.dataset.url === state.mediaUrl; x.classList.toggle('on', on); if (on) sel = x; }); state.showroomSelected = !!(sel && sel.dataset.showroom === '1'); updateShowroomNote(); });
 
     async function loadMusic() {
       try {
@@ -137,7 +186,8 @@ window.MCC_PANELS.quickpost = {
       const media=state.mediaUrl ? (isVideo?`<video src="${esc(state.mediaUrl)}" controls playsinline></video>`:`<img src="${esc(state.mediaUrl)}" alt="Chosen post picture">`) : '<div class="kp-wait">No picture</div>';
       const tags=state.channels.map(c=>`<span class="kp-tag">${icons[c]||'●'} ${esc(c)}</span>`).join('');
       const song = state.song ? `${esc(state.song.name)} · ${esc(state.song.duration)}<div class="kp-disclosure">Music suggestion only. It is saved with the post recipe; some social apps require adding the track inside their own app.</div>` : 'No music';
-      $('#kp-review').innerHTML=`<div class="kp-preview">${media}</div><div class="kp-summary"><h4>Your words</h4><p>${esc(cap||'No words')}</p><h4>Music suggestion</h4><p>${song}</p><h4>Posting to</h4><div class="kp-tags">${tags||'<span class="kp-tag">None chosen</span>'}</div></div>`;
+      const showroomWarn = state.showroomSelected ? `<div class="kp-showroom-note" style="margin:0 0 14px"><b>🏛️ Showroom-only line</b>Make sure the words invite people to the Designer Wallcoverings showroom (see it in person / book a visit) — not to shop or buy online.</div>` : '';
+      $('#kp-review').innerHTML=`<div class="kp-preview">${media}</div><div class="kp-summary">${showroomWarn}<h4>Your words</h4><p>${esc(cap||'No words')}</p><h4>Music suggestion</h4><p>${song}</p><h4>Posting to</h4><div class="kp-tags">${tags||'<span class="kp-tag">None chosen</span>'}</div></div>`;
       $('#kp-confirm').checked=false; $('#kp-publish').disabled=true; $('#kp-result').textContent='';
     }
     $('#kp-confirm').onchange=e => { $('#kp-publish').disabled=!e.target.checked; };

← d3e2d6d auto-data-snapshot: 2026-09-03T11:10:51 (1 data files) — pub  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-09-03T11:49:57 (1 data files) — pub 5430cac →