[object Object]

← back to Rentv Sheet Enrich

Live console: Width presets (Tight/Cozy/Wide/Fit + auto-refit on window resize) + Text size S/M/L, replacing the raw density slider; fix column drag-to-reorder (preventDefault stops text-selection hijack)

00fc8a62949e6f8cc1696eae546222af93a110f0 · 2026-08-13 16:27:15 -0700 · Steve Abrams

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

Files touched

Diff

commit 00fc8a62949e6f8cc1696eae546222af93a110f0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 16:27:15 2026 -0700

    Live console: Width presets (Tight/Cozy/Wide/Fit + auto-refit on window resize) + Text size S/M/L, replacing the raw density slider; fix column drag-to-reorder (preventDefault stops text-selection hijack)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public_live/index.html | 39 +++++++++++++++++++++++++++++++++------
 1 file changed, 33 insertions(+), 6 deletions(-)

diff --git a/public_live/index.html b/public_live/index.html
index 86a83e2..32c8e78 100644
--- a/public_live/index.html
+++ b/public_live/index.html
@@ -32,6 +32,7 @@
   button{cursor:pointer} button:hover{border-color:var(--accent)}
   .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
   .seg button{border:0;border-radius:0;background:var(--panel2)} .seg button.on{background:var(--accent);color:#04121f;font-weight:700}
+  .segsm{flex:1} .segsm button{flex:1;padding:5px 8px;font-size:11px} .segsm button+button{border-left:1px solid var(--line)}
   .grow{flex:1}
   .sync{display:flex;align-items:center;gap:7px;color:var(--dim);font-size:12px}
   .dot{width:9px;height:9px;border-radius:50%;background:#3fb950} .dot.stale{background:#d29922} .dot.err{background:#f85149}
@@ -201,6 +202,7 @@ let FILTERS={}, SEARCH='', SORT=LS.get('sort',null), GROUP=LS.get('group',''), H
 // the Group dropdown still works within a session for anyone who wants it; it just won't stick across refreshes.
 GROUP=''; LS.set('group','');
 let LOOKUP=null;   // when set to a column index, the main area shows that field's full value lookup
+let WIDTHPRESET=LS.get('widthpreset','');   // '', tight, cozy, wide, fit — 'fit' re-fits columns to the window on resize
 const MAXROWS=2500;
 function pk(){return 'p'+GID}  // per-tab localStorage namespace
 
@@ -291,8 +293,8 @@ function buildRail(){
     <div class="rrow"><label>Sort</label><select id="sortsel"><option value="">— none —</option>${opts}</select>
       <button id="sortdir" title="asc/desc">${SORT&&SORT.dir<0?'▼':'▲'}</button></div>
     <div class="rrow"><label>Group</label><select id="groupsel"><option value="">— none —</option>${opts}</select></div>
-    <div class="rrow"><label>Density</label><input type="range" id="dens" min="10" max="18" value="${getComputedStyle(document.documentElement).getPropertyValue('--fs').trim().replace('px','')||13}"></div>
-    <div class="rrow"><label>Columns</label><button id="fitcols" title="size every column to fit its content">⤢ Fit to content</button><button id="resetcols" title="reset widths + order to default">↺ Reset</button></div>
+    <div class="rrow"><label>Width</label><div class="seg segsm" id="wseg" title="column width"><button data-w="tight">Tight</button><button data-w="cozy">Cozy</button><button data-w="wide">Wide</button><button data-w="fit" title="fit all columns on screen (re-fits when you resize the window)">Fit</button></div></div>
+    <div class="rrow"><label>Text</label><div class="seg segsm" id="tseg" title="text size"><button data-t="12">S</button><button data-t="14">M</button><button data-t="16">L</button></div><button id="fitcols" title="size each column to its content">⤢ Autofit</button><button id="resetcols" title="reset widths + order">↺ Reset</button></div>
     <details class="sec" open><summary>Fields (${H.filter(Boolean).length}) · <span class="mini" id="allf">all</span> / <span class="mini" id="nonef">none</span></summary>
       <div id="ftogs"></div></details>
     <details class="sec" open><summary>Filters — click a value</summary><div id="facets"></div></details>`;
@@ -366,8 +368,11 @@ function wireRail(){
   $('#sortsel').onchange=e=>{SORT=e.target.value===''?null:{col:+e.target.value,dir:SORT?SORT.dir:1};LS.set('sort.'+curKey(),SORT);render()};
   $('#sortdir').onclick=()=>{if(SORT){SORT.dir*=-1;LS.set('sort.'+curKey(),SORT);$('#sortdir').textContent=SORT.dir<0?'▼':'▲';render()}};
   $('#groupsel').onchange=e=>{GROUP=e.target.value;LS.set('group',GROUP);render()};
-  $('#dens').oninput=e=>{document.documentElement.style.setProperty('--fs',e.target.value+'px');LS.set('dens',e.target.value)};
-  $('#fitcols').onclick=fitAllCols; $('#resetcols').onclick=resetLayout;
+  $$('#wseg button').forEach(b=>b.onclick=()=>setWidthPreset(b.dataset.w));
+  $$('#tseg button').forEach(b=>b.onclick=()=>{document.documentElement.style.setProperty('--fs',b.dataset.t+'px');LS.set('dens',b.dataset.t);markTextBtns()});
+  $('#fitcols').onclick=()=>{WIDTHPRESET='';LS.set('widthpreset','');fitAllCols();markWidthBtns()};
+  $('#resetcols').onclick=()=>{WIDTHPRESET='';LS.set('widthpreset','');resetLayout()};
+  markWidthBtns(); markTextBtns();
   $('#alls').onclick=()=>{SELECTED=new Set(META.tabs.map(t=>t.gid));loadData()};
   $('#nones').onclick=()=>{SELECTED=new Set([GID]);loadData()};
   $('#allf').onclick=()=>{HIDDEN.clear();LS.set('hidden.'+curKey(),[]);buildRail();render()};
@@ -420,6 +425,23 @@ function reorder(from,to){   // shared: move column 'from' to 'to's slot; used b
   ORDER=order; LS.set('order.'+curKey(),ORDER); render(); renderFields();   // sync BOTH views
 }
 const moveCol=reorder, moveFieldTo=reorder;
+// WIDTH PRESETS: one click sizes every visible column. 'fit' also re-fits on window resize.
+function setWidthPreset(mode){
+  WIDTHPRESET=mode; LS.set('widthpreset',mode);
+  const cols=visCols();
+  if(mode==='fit'){ applyFit(); }
+  else if(mode==='cozy'){ COLW={}; LS.set('colw.'+curKey(),COLW); render(); }
+  else{ const w=mode==='tight'?120:280; cols.forEach(c=>COLW[c.i]=w); LS.set('colw.'+curKey(),COLW); render(); }
+  markWidthBtns();
+}
+function applyFit(){   // distribute the viewport width across visible columns so the whole table fits on screen
+  const cols=visCols(); if(!cols.length)return;
+  const wrap=$('#wrap'); const avail=((wrap&&wrap.clientWidth)||window.innerWidth)-44-24;   // action col + scrollbar/padding
+  const w=Math.max(64, Math.floor(avail/cols.length));
+  cols.forEach(c=>COLW[c.i]=w); LS.set('colw.'+curKey(),COLW); render();
+}
+function markWidthBtns(){ const b=document.querySelectorAll('#wseg button'); b.forEach(x=>x.classList.toggle('on',x.dataset.w===WIDTHPRESET)); }
+function markTextBtns(){ const fs=(getComputedStyle(document.documentElement).getPropertyValue('--fs').trim().replace('px','')||'14'); document.querySelectorAll('#tseg button').forEach(x=>x.classList.toggle('on',x.dataset.t===fs)); }
 function fitAllCols(){   // expand every visible column to fit its content
   visCols().forEach(c=>{ let max=String(DATA.headers[c.i]||'').length;
     document.querySelectorAll(`#grid td[data-c="${c.i}"]`).forEach(td=>{const t=td.textContent||'';if(t.length>max)max=t.length});
@@ -521,12 +543,15 @@ function wireGrid(){
   // header: click = sort, drag = MOVE column (manual drag, 5px threshold; order persists per tab)
   $$('#grid thead th[data-c]').forEach(th=>th.addEventListener('mousedown',e=>{
     if(e.target.classList.contains('rz'))return;                 // resize handle is separate
+    e.preventDefault();                                          // stop text-selection from hijacking the drag
+    document.body.style.cursor='grabbing';
     const startX=e.clientX, col=+th.dataset.c, ths=[...document.querySelectorAll('#grid thead th[data-c]')];
     let moved=false;
     const at=x=>ths.find(t=>{const r=t.getBoundingClientRect();return x>=r.left&&x<=r.right});
     const mv=ev=>{ if(!moved&&Math.abs(ev.clientX-startX)>5){moved=true;th.classList.add('dragging')}
       if(moved){ths.forEach(t=>t.classList.remove('droptarget'));const tg=at(ev.clientX);if(tg&&tg!==th)tg.classList.add('droptarget')} };
     const up=ev=>{ document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up);
+      document.body.style.cursor='';
       th.classList.remove('dragging');ths.forEach(t=>t.classList.remove('droptarget'));
       if(moved){const tg=at(ev.clientX);if(tg&&+tg.dataset.c!==col)moveCol(col,+tg.dataset.c);}
       else{SORT=(SORT&&SORT.col===col)?{col,dir:-SORT.dir}:{col,dir:1};LS.set('sort.'+curKey(),SORT);render();$('#sortsel')&&($('#sortsel').value=col);} };
@@ -545,7 +570,7 @@ function wireGrid(){
     const mv=ev=>{ newW=Math.max(60,startW+(ev.clientX-startX)); guide.style.left=(startX+(newW-startW))+'px'; };
     const up=()=>{ document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up);
       guide.remove(); document.body.classList.remove('resizing');
-      COLW[ci]=Math.round(newW); LS.set('colw.'+curKey(),COLW); render(); };  // full re-render = exact width, no re-proportioning
+      COLW[ci]=Math.round(newW); WIDTHPRESET=''; LS.set('widthpreset',''); LS.set('colw.'+curKey(),COLW); render(); markWidthBtns(); };  // manual width exits any preset
     document.addEventListener('mousemove',mv);document.addEventListener('mouseup',up);
   }));
   // double-click a resize handle to auto-fit the column to its content
@@ -628,8 +653,10 @@ $('#act-copy').onclick=()=>{navigator.clipboard?.writeText(($('#d-subject').valu
 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', (window.matchMedia&&matchMedia('(prefers-color-scheme: light)').matches)?'light':'dark'));
+// Fit mode = table always fits the window: re-fit columns to the viewport when it resizes
+window.addEventListener('resize',()=>{ if(WIDTHPRESET==='fit'){ clearTimeout(window._rzfit); window._rzfit=setTimeout(applyFit,120); } });
 document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDraft()});
-(async()=>{await loadMeta();await loadData()})();
+(async()=>{await loadMeta();await loadData(); if(WIDTHPRESET==='fit')applyFit();})();
 </script>
 </body>
 </html>

← 2a21996 Live console: FORCE grouping off on every load (kills the bl  ·  back to Rentv Sheet Enrich  ·  Live console: email cells are now clickable mailto links; co dfed233 →