[object Object]

← back to Rentv

crm: open contact action links inline in a right-dock iframe panel (like desk); frame-hostile sites (Google/LinkedIn/FB/X) fall back to open-in-tab

a364d190599c3f2b5c36280dde174aa8e8b158a0 · 2026-08-12 16:40:43 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a364d190599c3f2b5c36280dde174aa8e8b158a0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 12 16:40:43 2026 -0700

    crm: open contact action links inline in a right-dock iframe panel (like desk); frame-hostile sites (Google/LinkedIn/FB/X) fall back to open-in-tab
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/crm.html | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 63 insertions(+), 1 deletion(-)

diff --git a/public/crm.html b/public/crm.html
index c18ed7bc..380d71b2 100644
--- a/public/crm.html
+++ b/public/crm.html
@@ -76,6 +76,22 @@
   .seg-chip{background:#0e141a;border:1px solid #2a333d;border-radius:8px;padding:4px 9px;font-size:11.5px;font-weight:700;color:#c3cbd3}
   .empty{padding:50px 20px;text-align:center;color:#6b747d}
   .hint{padding:9px 14px;font-size:12px;color:#8b939b;background:#10161d;border-bottom:1px solid var(--line)}
+  /* ── inline preview panel (right dock) ── opens web action links in an iframe instead of a new tab */
+  .pv-scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:.2s;z-index:970}.pv-scrim.open{opacity:1;pointer-events:auto}
+  .pv{position:fixed;top:0;right:0;bottom:0;width:46vw;min-width:380px;max-width:820px;background:#0b0e12;border-left:1px solid var(--line);transform:translateX(102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:971;display:flex;flex-direction:column;box-shadow:-2px 0 44px rgba(0,0,0,.5)}
+  .pv.open{transform:none}
+  .pv .pvh{display:flex;align-items:center;gap:9px;padding:10px 12px;border-bottom:1px solid var(--line);background:#0d1014}
+  .pv .pvh .t{font-weight:800;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:60px}
+  .pv .pvh .u{font-size:11px;color:#6b747d;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:34%}
+  .pv .pvh a,.pv .pvh button{background:#12161c;border:1px solid #2a333d;border-radius:8px;color:#dbe1e7;font:700 12px 'Inter';padding:6px 10px;cursor:pointer;white-space:nowrap;text-decoration:none;line-height:1}.pv .pvh a:hover,.pv .pvh button:hover{border-color:var(--red);color:#fff}
+  .pv .pvbody{position:relative;flex:1;background:#fff;overflow:hidden}
+  .pv iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff}
+  .pv .pvstate{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;gap:14px;text-align:center;padding:30px;background:#0d1014;color:#c3cbd3;z-index:2}
+  .pv .pvbody.loading .pvload,.pv .pvbody.blocked .pvblock{display:flex}
+  .pv .pvstate .big{font-size:16px;font-weight:800}.pv .pvstate .sm{font-size:12.5px;color:#8b939b;max-width:360px;line-height:1.55}
+  .pv .pvstate a.open{background:var(--red);border:0;border-radius:9px;color:#fff;font:800 13px 'Inter';padding:11px 18px;text-decoration:none}.pv .pvstate a.open:hover{filter:brightness(1.08)}
+  .pv .spin{width:26px;height:26px;border:3px solid #232a32;border-top-color:var(--red);border-radius:50%;animation:pvspin .8s linear infinite}@keyframes pvspin{to{transform:rotate(360deg)}}
+  @media(max-width:900px){.pv{width:100vw;max-width:none;min-width:0}.pv .pvh .u{display:none}}
 </style>
 </head>
 <body>
@@ -135,6 +151,25 @@
   </div>
 </div>
 
+<!-- inline preview: right-dock iframe panel -->
+<div class="pv-scrim" id="pvScrim"></div>
+<aside class="pv" id="pv" aria-hidden="true">
+  <div class="pvh">
+    <span class="t" id="pvTitle">Preview</span>
+    <span class="u" id="pvUrl"></span>
+    <a id="pvOpen" href="#" target="_blank" rel="noopener noreferrer">Open ↗</a>
+    <button id="pvClose" aria-label="Close preview">✕</button>
+  </div>
+  <div class="pvbody" id="pvBody">
+    <div class="pvstate pvload"><div class="spin"></div><div class="sm">Loading inline preview…</div></div>
+    <div class="pvstate pvblock">
+      <div class="big">🔒 This site blocks inline preview</div>
+      <div class="sm">Some sites (LinkedIn, Google, Facebook/X) refuse to load inside an embedded frame for security. Open it in a new tab instead — the CRM stays here behind it.</div>
+      <a class="open" id="pvBlockOpen" href="#" target="_blank" rel="noopener noreferrer">Open in new tab ↗</a>
+    </div>
+    <iframe id="pvFrame" referrerpolicy="strict-origin-when-cross-origin" allow="fullscreen" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"></iframe>
+  </div>
+</aside>
 <script>
 const $=s=>document.getElementById(s), enc=encodeURIComponent;
 const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
@@ -146,7 +181,7 @@ const MAPS=q=>`https://www.google.com/maps/search/?api=1&query=${enc(q)}`;
 const DRE=l=>`https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=${enc(l)}`;
 const fmtWhen=d=>{ if(!d) return '—'; const t=new Date(d); if(isNaN(t)) return '—';
   return t.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); };
-const A=(h,i,l)=>h?`<a href="${h}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()"><span>${i}</span>${l}</a>`:'';
+const A=(h,i,l)=>h?`<a class="act" href="${h}" target="_blank" rel="noopener noreferrer"${/^https?:\/\//i.test(h)?' data-pv="1"':''} onclick="event.stopPropagation()"><span>${i}</span>${l}</a>`:'';
 
 // ── persisted UI state ──
 const LS=(k,d)=>{try{const v=localStorage.getItem('crm.'+k);return v==null?d:v;}catch{return d;}};
@@ -305,6 +340,33 @@ $('sort').onchange=e=>{ S.sort=e.target.value; setLS('sort',S.sort); render(); }
 $('dens').oninput=e=>{ S.dens=+e.target.value; setLS('dens',S.dens); applyDensity(); };
 document.querySelectorAll('#modeToggle button').forEach(b=>b.onclick=()=>{ S.mode=b.dataset.mode; setLS('mode',S.mode); render(); });
 
+// ── inline preview panel ── route web action links into a right-dock iframe instead of a new tab
+const PV={t:null};
+function pvEmbed(u){ try{const m=u.match(/[?&]query=([^&]+)/);if(/google\.[^/]+\/maps\/search/i.test(u)&&m)return 'https://maps.google.com/maps?q='+m[1]+'&z=14&output=embed';}catch(e){} return u; }
+const PV_HARD=[/google\.[a-z.]+\/search/i,/linkedin\.com/i,/facebook\.com/i,/(^|\/\/)(www\.)?(x|twitter)\.com/i,/instagram\.com/i];
+function pvHardBlocked(u){return PV_HARD.some(re=>re.test(u));}
+function pvClose(){clearTimeout(PV.t);$('pvFrame').onload=null;$('pv').classList.remove('open');$('pvScrim').classList.remove('open');$('pv').setAttribute('aria-hidden','true');$('pvFrame').src='about:blank';$('pvBody').classList.remove('loading','blocked');}
+function pvOpen(href,label){
+  const body=$('pvBody'),fr=$('pvFrame');
+  $('pvTitle').textContent=label||'Preview';
+  try{$('pvUrl').textContent=new URL(href).hostname.replace(/^www\./,'');}catch(e){$('pvUrl').textContent='';}
+  $('pvOpen').href=href;$('pvBlockOpen').href=href;
+  clearTimeout(PV.t);fr.onload=null;body.classList.remove('loading','blocked');
+  $('pv').classList.add('open');$('pvScrim').classList.add('open');$('pv').setAttribute('aria-hidden','false');
+  if(pvHardBlocked(href)){fr.src='about:blank';body.classList.add('blocked');return;}
+  body.classList.add('loading');
+  fr.onload=()=>{clearTimeout(PV.t);body.classList.remove('loading','blocked');};
+  fr.src=pvEmbed(href);
+  PV.t=setTimeout(()=>{if(body.classList.contains('loading')){body.classList.remove('loading');body.classList.add('blocked');}},4200);
+}
+$('pvClose').onclick=pvClose;$('pvScrim').onclick=pvClose;
+document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('pv').classList.contains('open'))pvClose();});
+document.addEventListener('click',e=>{ // plain left-click on an action link → inline panel; cmd/ctrl/middle-click still opens a native new tab
+  const a=e.target.closest('a.act[data-pv]');if(!a)return;
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0)return;
+  e.preventDefault();e.stopPropagation();pvOpen(a.getAttribute('href'),(a.textContent||'').trim());
+},true);
+
 load();
 </script>
 </body></html>

← 14cfd3dd auto-data-snapshot: 2026-08-12T16:33:05 (7 data files) — dat  ·  back to Rentv  ·  crm: full-width Table view — every scanned field a sortable ae07d2ff →