[object Object]

← back to Rentv Sheet Enrich

Console: resizable columns (drag edge, persists per tab via colgroup+table-layout:fixed) + night/day theme toggle (persisted) + confirm LinkedIn cells href to exact company/profile URL

6f48a2ba399bd545f9b568c17b764fc6313f0c9a · 2026-08-13 14:11:56 -0700 · Steve Abrams

Files touched

Diff

commit 6f48a2ba399bd545f9b568c17b764fc6313f0c9a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 14:11:56 2026 -0700

    Console: resizable columns (drag edge, persists per tab via colgroup+table-layout:fixed) + night/day theme toggle (persisted) + confirm LinkedIn cells href to exact company/profile URL
---
 public_live/index.html | 29 +++++++++++++++++++++++++----
 1 file changed, 25 insertions(+), 4 deletions(-)

diff --git a/public_live/index.html b/public_live/index.html
index 92fd62a..45de2e1 100644
--- a/public_live/index.html
+++ b/public_live/index.html
@@ -11,6 +11,7 @@
     --dim:#9aa7b4; --accent:#4c9be8; --green:#cbe6b4; --greenink:#1a2e10; --chip:#232c37; --fs:13px; --rail:300px;
   }
   *{box-sizing:border-box}
+  body.light{--bg:#f4f6f9;--panel:#ffffff;--panel2:#eef2f6;--line:#d3dbe4;--ink:#1a2230;--dim:#5c6b7a;--chip:#e7edf3;--green:#dff0cf;--greenink:#26401a;}
   body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--ink);font-size:var(--fs)}
   header{position:sticky;top:0;z-index:40;background:var(--panel);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:12px;padding:9px 14px;flex-wrap:wrap}
   header h1{font-size:15px;margin:0;font-weight:700;letter-spacing:.3px;white-space:nowrap}
@@ -55,8 +56,12 @@
   .mini{font-size:10px;color:var(--dim);cursor:pointer} .mini:hover{color:var(--accent)}
   /* table */
   table{border-collapse:collapse;width:100%;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden}
-  thead th{position:sticky;top:0;background:var(--panel2);text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);font-size:11px;color:var(--dim);white-space:nowrap;z-index:5;cursor:pointer}
+  thead th{position:sticky;top:0;background:var(--panel2);text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);font-size:11px;color:var(--dim);white-space:nowrap;z-index:5;cursor:pointer;position:sticky;overflow:hidden;text-overflow:ellipsis}
   thead th:hover{color:var(--accent)}
+  th .rz{position:absolute;right:0;top:0;height:100%;width:7px;cursor:col-resize;z-index:6}
+  th .rz:hover{background:var(--accent)}
+  table{table-layout:fixed}
+  td{overflow:hidden;text-overflow:ellipsis}
   tbody td{padding:6px 9px;border-bottom:1px solid var(--line);vertical-align:top;max-width:320px}
   tbody tr:hover{background:#1a212b}
   td[contenteditable]:focus{outline:none;background:#0d2b3f;box-shadow:inset 0 0 0 2px var(--accent)}
@@ -98,6 +103,7 @@
   <span class="chips" id="chips"></span>
   <span class="grow"></span>
   <span class="sync"><span class="dot" id="dot"></span><span id="syncmsg">loading…</span></span>
+  <button id="theme" title="Night / Day">🌙</button>
   <button id="refresh" title="Rebuild local copy from the sheet">↻ Refresh</button>
 </header>
 
@@ -131,7 +137,7 @@ const $=s=>document.querySelector(s), $$=s=>document.querySelectorAll(s);
 const LS={get:(k,d)=>{try{return JSON.parse(localStorage.getItem('rc2.'+k))??d}catch(e){return d}},set:(k,v)=>localStorage.setItem('rc2.'+k,JSON.stringify(v))};
 let DATA={gid:0,title:'',headers:[],rows:[],green:[]}, GREENSET=new Set(), ROLE={}, META=null;
 let VIEW=LS.get('view','table'), GID=LS.get('gid',null), editing=false, curRow=null, curChan='email';
-let FILTERS={}, SEARCH='', SORT=LS.get('sort',null), GROUP=LS.get('group',''), HIDDEN=new Set(LS.get('hidden.'+(GID||''),[]));
+let FILTERS={}, SEARCH='', SORT=LS.get('sort',null), GROUP=LS.get('group',''), HIDDEN=new Set(LS.get('hidden.'+(GID||''),[])), COLW={};
 const MAXROWS=2500;
 function pk(){return 'p'+GID}  // per-tab localStorage namespace
 
@@ -161,6 +167,7 @@ async function loadTab(){
   DATA=d; GREENSET=new Set(d.green||[]); detectRoles();
   FILTERS={}; SEARCH=''; $('#search')&&($('#search').value='');
   HIDDEN=new Set(LS.get('hidden.'+GID,[]));
+  COLW=LS.get('colw.'+GID,{});
   // default sort = Last Name ascending on load (if the tab has one)
   const lnIdx=DATA.headers.findIndex(h=>/^last name$/i.test(String(h).trim()));
   SORT = lnIdx>=0 ? {col:lnIdx, dir:1} : null;
@@ -256,7 +263,8 @@ function render(){
 }
 function tableHTML(rows){
   const cols=visCols();
-  let h='<table id="grid"><thead><tr><th></th>'+cols.map(c=>`<th data-c="${c.i}">${esc(c.h)}${SORT&&SORT.col===c.i?(SORT.dir<0?' ▼':' ▲'):''}</th>`).join('')+'</tr></thead><tbody>';
+  const cg='<colgroup><col style="width:44px">'+cols.map(c=>`<col style="width:${(COLW[c.i]||180)}px">`).join('')+'</colgroup>';
+  let h='<table id="grid">'+cg+'<thead><tr><th></th>'+cols.map(c=>`<th data-c="${c.i}">${esc(c.h)}${SORT&&SORT.col===c.i?(SORT.dir<0?' ▼':' ▲'):''}<span class="rz" data-c="${c.i}"></span></th>`).join('')+'</tr></thead><tbody>';
   let lastG=null;
   for(const [r,ri] of rows){
     if(GROUP!==''){ const gv=g(r,+GROUP)||'(blank)'; if(gv!==lastG){ lastG=gv; h+=`<tr class="grouphdr"><td colspan="${cols.length+1}">${esc(gv)}</td></tr>`; } }
@@ -282,7 +290,17 @@ function contact(r){const nm=g(r,ROLE.name),co=g(r,ROLE.company);return{name:nm,
 function wireGrid(){
   $$('.rowbtn').forEach(b=>b.onclick=e=>{e.stopPropagation();openDraft(+b.dataset.r)});
   $$('.card').forEach(c=>c.onclick=()=>openDraft(+c.dataset.r));
-  $$('#grid thead th[data-c]').forEach(th=>th.onclick=()=>{const ci=+th.dataset.c;SORT=(SORT&&SORT.col===ci)?{col:ci,dir:-SORT.dir}:{col:ci,dir:1};LS.set('sort',SORT);render();$('#sortsel')&&($('#sortsel').value=ci)});
+  $$('#grid thead th[data-c]').forEach(th=>th.onclick=e=>{if(e.target.classList.contains('rz'))return;const ci=+th.dataset.c;SORT=(SORT&&SORT.col===ci)?{col:ci,dir:-SORT.dir}:{col:ci,dir:1};LS.set('sort',SORT);render();$('#sortsel')&&($('#sortsel').value=ci)});
+  // resizable columns — drag a column's right edge; width persists per tab
+  $$('#grid th .rz').forEach(rz=>rz.addEventListener('mousedown',e=>{
+    e.preventDefault();e.stopPropagation();
+    const ci=+rz.dataset.c, cols=visCols(), idx=cols.findIndex(c=>c.i===ci);
+    const col=document.querySelectorAll('#grid colgroup col')[idx+1];  // +1 for the action col
+    const startX=e.clientX, startW=col.getBoundingClientRect().width;
+    const mv=ev=>{const w=Math.max(48,startW+(ev.clientX-startX));col.style.width=w+'px';COLW[ci]=Math.round(w);};
+    const up=()=>{document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up);LS.set('colw.'+GID,COLW);};
+    document.addEventListener('mousemove',mv);document.addEventListener('mouseup',up);
+  }));
   $$('td[contenteditable]').forEach(td=>{td.addEventListener('focus',()=>{editing=true;td._o=td.textContent});td.addEventListener('blur',()=>saveCell(td));td.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();td.blur()}})});
 }
 async function saveCell(td){ editing=false; const v=td.textContent.trim(); if(v===String(td._o||'').trim())return;
@@ -329,6 +347,9 @@ $('#dclose').onclick=closeDraft;$('#scrim').onclick=closeDraft;
 $$('#chanseg button').forEach(b=>b.onclick=()=>{if(b.classList.contains('chdis'))return;curChan=b.dataset.c;syncChan();regen()});
 $('#purpose').onchange=regen;$('#tone').onchange=regen;$('#regen').onclick=regen;$('#enhance').onclick=doEnhance;$('#act-go').onclick=doGo;
 $('#act-copy').onclick=()=>{navigator.clipboard?.writeText(($('#d-subject').value?('Subject: '+$('#d-subject').value+'\n\n'):'')+$('#d-body').value);$('#act-copy').textContent='✓ Copied';setTimeout(()=>$('#act-copy').textContent='⧉ Copy draft',1200)};
+function applyTheme(t){document.body.classList.toggle('light',t==='light');$('#theme').textContent=t==='light'?'☀️':'🌙'}
+$('#theme').onclick=()=>{const t=document.body.classList.contains('light')?'dark':'light';LS.set('theme',t);applyTheme(t)};
+applyTheme(LS.get('theme','dark'));
 document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDraft()});
 (async()=>{await loadMeta();await loadTab()})();
 </script>

← ea0de54 Sort: blanks sink to bottom so real Last Names lead A->Z  ·  back to Rentv Sheet Enrich  ·  All LinkedIn + Website cells href to exact site: cellRender c6e12e8 →