← 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
M data/build.jsonM public/index.html
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 →