[object Object]

← back to Dw Photo Capture

Add ✂️ Crop tool to photo editor (drag corners/move, dim outside, bakes into final) + default landing = 🧵 All TWIL + Any-SKU auto-falls-back to all-Shopify (mfr#/DW SKU) when not in TWIL index

f97337accf9add1226dbb7a070130ebdc9e2d5ef · 2026-06-25 09:51:46 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit f97337accf9add1226dbb7a070130ebdc9e2d5ef
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Thu Jun 25 09:51:46 2026 -0700

    Add ✂️ Crop tool to photo editor (drag corners/move, dim outside, bakes into final) + default landing = 🧵 All TWIL + Any-SKU auto-falls-back to all-Shopify (mfr#/DW SKU) when not in TWIL index
---
 data/build.json   |  5 ++--
 public/index.html | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 83 insertions(+), 6 deletions(-)

diff --git a/data/build.json b/data/build.json
index 6f69547..fa23cc5 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
 {
-  "next": 32,
+  "next": 33,
   "map": {
     "63863152": 27,
     "578af86f": 2,
@@ -30,6 +30,7 @@
     "8a1a955f": 28,
     "f4a0bf6d": 29,
     "86060b11": 30,
-    "43b1741c": 31
+    "43b1741c": 31,
+    "b2f94d6e": 32
   }
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 7925614..de92ceb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -122,6 +122,21 @@
   .sl label{font-size:12px;color:var(--muted)}
   .sl input[type=range]{width:100%;accent-color:var(--gold)}
   .sl .v{font:600 12px/1 ui-monospace,Menlo,monospace;color:var(--ink);text-align:right}
+  .ed-crop{background:#16140f;border:1px solid var(--gold);color:var(--gold)}
+  /* crop overlay */
+  .cropov{position:fixed;inset:0;z-index:120}
+  .cropov[hidden]{display:none}
+  .cropbox{position:fixed;border:1px solid #fff;box-shadow:0 0 0 4000px rgba(0,0,0,.55);cursor:move;touch-action:none;
+    background-image:linear-gradient(rgba(255,255,255,.25) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.25) 1px,transparent 1px);background-size:33.33% 33.33%}
+  .cropbox i{position:absolute;width:26px;height:26px;border:3px solid var(--gold);background:rgba(0,0,0,.25);box-sizing:border-box;touch-action:none}
+  .cropbox i[data-h=nw]{left:-3px;top:-3px;border-right:0;border-bottom:0;cursor:nwse-resize}
+  .cropbox i[data-h=ne]{right:-3px;top:-3px;border-left:0;border-bottom:0;cursor:nesw-resize}
+  .cropbox i[data-h=sw]{left:-3px;bottom:-3px;border-right:0;border-top:0;cursor:nesw-resize}
+  .cropbox i[data-h=se]{right:-3px;bottom:-3px;border-left:0;border-top:0;cursor:nwse-resize}
+  .crop-bar{position:fixed;left:50%;bottom:max(24px,env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;gap:12px;z-index:121}
+  .crop-bar button{border:none;border-radius:12px;padding:14px 26px;font-weight:700;font-size:15px;cursor:pointer}
+  #cropCancel{background:#16140f;border:1px solid var(--red);color:var(--red)}
+  #cropApply{background:var(--gold);color:#1b1407}
   .ed-actions{display:flex;gap:10px;margin-top:14px}
   .ed-actions button{flex:1;border:none;border-radius:12px;padding:14px;font-weight:700;font-size:15px;cursor:pointer}
   .ed-reset{background:#16140f;border:1px solid var(--line);color:var(--muted)}
@@ -146,11 +161,11 @@
   </div>
   <div class="controls">
     <div class="chips">
-      <div class="chip on" data-f="need">Needs photo</div>
+      <div class="chip" data-f="need">Needs photo</div>
       <div class="chip" data-f="live">🟢 Live</div>
       <div class="chip" data-f="done">Done</div>
       <div class="chip" data-f="new" id="chipNew">✨ New</div>
-      <div class="chip" data-f="twil" id="chipTwil">🧵 All TWIL</div>
+      <div class="chip on" data-f="twil" id="chipTwil">🧵 All TWIL</div>
       <div class="chip" data-f="all">All</div>
       <div class="chip" data-f="any">🔎 Any SKU</div>
       <div class="chip" data-f="shop">🌐 All Shopify</div>
@@ -204,15 +219,22 @@
       </div>
     </div>
     <div class="ed-actions">
+      <button class="ed-crop" id="edCrop">✂️ Crop</button>
       <button class="ed-reset" id="edReset">Reset</button>
       <button class="ed-cancel" id="edCancel">Cancel</button>
       <button class="ed-use" id="edUse">✓ Use Photo</button>
     </div>
+
+<!-- crop overlay (Photoshop-style: drag handles, dim outside) -->
+<div class="cropov" id="cropov" hidden>
+  <div class="cropbox" id="cropbox"><i data-h="nw"></i><i data-h="ne"></i><i data-h="sw"></i><i data-h="se"></i></div>
+  <div class="crop-bar"><button id="cropCancel">✕ Cancel</button><button id="cropApply">✓ Apply Crop</button></div>
+</div>
   </div>
 </div>
 <script>
 const $=s=>document.querySelector(s), grid=$('#grid');
-let ITEMS=[], filter='need', collapsed=true; // start collapsed — clean landing until a filter/search/scan
+let ITEMS=[], filter='twil', collapsed=false; // default landing = All TWIL
 const LS=k=>localStorage.getItem('dwphoto_'+k), setLS=(k,v)=>localStorage.setItem('dwphoto_'+k,v);
 
 // Density: set grid-template-columns DIRECTLY (older iOS Safari ignores var() as
@@ -243,7 +265,15 @@ async function doLookup(){
   if(!q){ LOOKUP=[]; render(); return; }
   try{ const r=await fetch('/api/lookup?q='+encodeURIComponent(q)); const d=await r.json();
     LOOKUP=(d.items||[]).map(it=>({...it, keep_images: !it.needs_create && !!it.image})); // preserve existing room/lifestyle shots on live SKUs
-    LOOKUP_INDEXED=d.indexed||0; render();
+    LOOKUP_INDEXED=d.indexed||0;
+    // not in the TWIL index? auto-escalate to a store-wide search (mfr# + DW SKU across every vendor)
+    if(!LOOKUP.length && q){
+      toast('Not in TWIL — searching all Shopify…');
+      document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on')); const sc=document.querySelector('.chip[data-f="shop"]'); if(sc)sc.classList.add('on');
+      filter='shop'; setLS('filter','shop'); $('#q').placeholder='🌐 Any Shopify SKU, name, vendor, or collection…';
+      return doShopSearch();
+    }
+    render();
   }catch(e){ toast('Lookup failed'); }
 }
 // The not-yet-in-Shopify sheet items — photograph one to create + go live.
@@ -675,6 +705,52 @@ $('#edReset').addEventListener('click',()=>{
   $('#edRoll').classList.remove('on'); $('#edRoll').textContent='🗞 Roll'; syncLabels(); edRedraw();
 });
 $('#edCancel').addEventListener('click',()=>{toast('Edit cancelled');closeEditor();});
+
+/* ===== Crop tool (drag corners / move box; dims outside) ===== */
+let CROP=null;
+function enterCrop(){
+  if(!ES)return;
+  const cv=$('#edcanvas'), r=cv.getBoundingClientRect();
+  CROP={r, iw:ES.img.width, ih:ES.img.height};
+  setCropBox(r.left+r.width*0.08, r.top+r.height*0.08, r.width*0.84, r.height*0.84);
+  $('#cropov').hidden=false;
+}
+function exitCrop(){ $('#cropov').hidden=true; CROP=null; }
+function setCropBox(x,y,w,h){
+  const b=$('#cropbox'); b.style.left=x+'px'; b.style.top=y+'px'; b.style.width=Math.max(30,w)+'px'; b.style.height=Math.max(30,h)+'px';
+}
+(function cropDrag(){
+  const box=$('#cropbox'); let mode=null,h=null,sx,sy,ox,oy,ow,oh;
+  const clampToCanvas=()=>{ if(!CROP)return; const r=CROP.r,bb=box.getBoundingClientRect();
+    let x=Math.max(r.left,Math.min(bb.left,r.right-bb.width)), y=Math.max(r.top,Math.min(bb.top,r.bottom-bb.height));
+    let w=Math.min(bb.width,r.right-x), ht=Math.min(bb.height,r.bottom-y); setCropBox(x,y,w,ht); };
+  function down(e){ if(!CROP)return; e.preventDefault(); const t=e.touches?e.touches[0]:e; sx=t.clientX; sy=t.clientY;
+    const bb=box.getBoundingClientRect(); ox=bb.left;oy=bb.top;ow=bb.width;oh=bb.height;
+    h=e.target.dataset&&e.target.dataset.h; mode=h?'resize':'move';
+    document.addEventListener('pointermove',move,{passive:false}); document.addEventListener('pointerup',up); }
+  function move(e){ if(!mode||!CROP)return; e.preventDefault(); const r=CROP.r; const dx=e.clientX-sx, dy=e.clientY-sy;
+    if(mode==='move'){ setCropBox(Math.max(r.left,Math.min(ox+dx,r.right-ow)), Math.max(r.top,Math.min(oy+dy,r.bottom-oh)), ow, oh); }
+    else { let x=ox,y=oy,w=ow,ht=oh;
+      if(h.includes('w')){ x=ox+dx; w=ow-dx; } if(h.includes('e')){ w=ow+dx; }
+      if(h.includes('n')){ y=oy+dy; ht=oh-dy; } if(h.includes('s')){ ht=oh+dy; }
+      if(w<30){ if(h.includes('w')) x=ox+ow-30; w=30; } if(ht<30){ if(h.includes('n')) y=oy+oh-30; ht=30; }
+      setCropBox(x,y,w,ht); clampToCanvas(); } }
+  function up(){ mode=null; document.removeEventListener('pointermove',move); document.removeEventListener('pointerup',up); }
+  box.addEventListener('pointerdown',down);
+})();
+$('#edCrop').addEventListener('click',enterCrop);
+$('#cropCancel').addEventListener('click',exitCrop);
+$('#cropApply').addEventListener('click',()=>{
+  if(!CROP||!ES)return; const r=CROP.r, bb=$('#cropbox').getBoundingClientRect();
+  const sx=(bb.left-r.left)/r.width*CROP.iw, sy=(bb.top-r.top)/r.height*CROP.ih;
+  const sw=bb.width/r.width*CROP.iw, sh=bb.height/r.height*CROP.ih;
+  const c=document.createElement('canvas'); c.width=Math.round(sw); c.height=Math.round(sh);
+  c.getContext('2d').drawImage(ES.img, sx,sy,sw,sh, 0,0,c.width,c.height);
+  const ni=new Image();
+  ni.onload=()=>{ ES.img=ni; ES.flat=document.createElement('canvas'); ES.flat.width=ni.width; ES.flat.height=ni.height;
+    const cv=$('#edcanvas'); cv.width=ni.width; cv.height=ni.height; edRedraw(); exitCrop(); toast('Cropped'); };
+  ni.src=c.toDataURL('image/jpeg',0.95);
+});
 $('#edUse').addEventListener('click',()=>{
   if(!ES)return; const x=ES.x;
   // BAKE the adjustment into pixels (CSS preview filter does NOT survive toDataURL).

← a2e7b32 Add 🧵 All TWIL chip (every Fentucci/grasscloth item, not ju  ·  back to Dw Photo Capture  ·  Crop tool: add aspect-ratio presets (Free / 1:1 / 4:3 / 3:4 3832493 →