[object Object]

← back to Dw Photo Capture

Front/Back tiles: tap a taken photo to adjust it in the Enhance/Crop editor; Adjust + Retake buttons below each tile (TK-12256)

69d50160659eb80e9ad45e980640137137bc7710 · 2026-09-25 12:22:23 -0700 · Steve Abrams

Editor/crop overlay raised above the add modal (z 130) so it opens from the tiles.
Adjusted pixels replace the local shot only — nothing uploads until Create.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZynS56oLsqJTcschug1a7

Files touched

Diff

commit 69d50160659eb80e9ad45e980640137137bc7710
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 25 12:22:23 2026 -0700

    Front/Back tiles: tap a taken photo to adjust it in the Enhance/Crop editor; Adjust + Retake buttons below each tile (TK-12256)
    
    Editor/crop overlay raised above the add modal (z 130) so it opens from the tiles.
    Adjusted pixels replace the local shot only — nothing uploads until Create.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TZynS56oLsqJTcschug1a7
---
 public/index.html | 44 ++++++++++++++++++++++++++++++++++----------
 1 file changed, 34 insertions(+), 10 deletions(-)

diff --git a/public/index.html b/public/index.html
index 21698d8..1ae78de 100644
--- a/public/index.html
+++ b/public/index.html
@@ -238,7 +238,7 @@
   .gal-empty{grid-column:1/-1;text-align:center;color:var(--muted);padding:40px 10px}
 
   /* ===== Photo editor (opens after capture, before upload) ===== */
-  .editor{position:fixed;inset:0;z-index:100;background:rgba(8,7,6,.97);display:flex;flex-direction:column}
+  .editor{position:fixed;inset:0;z-index:140;background:rgba(8,7,6,.97);display:flex;flex-direction:column}
   .editor[hidden]{display:none}
   .ed-stage{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:12px 12px 0;overflow:hidden}
   #edcanvas{max-width:100%;max-height:100%;border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.6);background:#000}
@@ -343,6 +343,12 @@
   .shot-btn.have .shot-ico{display:none}
   .shot-btn.have b,.shot-btn.have small,.shot-btn.have .shot-ico{position:relative;z-index:1;text-shadow:0 1px 3px #000,0 0 8px #000}
   .shot-btn.have .shot-num,.shot-btn.have .shot-ck::after{z-index:2}
+  /* TK-12256: tap a taken photo to adjust it; Adjust / Retake buttons under each tile */
+  .shot-acts{display:flex;gap:10px;margin:-2px 0 8px}
+  .shot-acts>div{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
+  .shot-acts button{flex:1;min-width:0;padding:0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:38px;border-radius:10px;border:1px solid var(--line);background:#16140f;color:var(--ink);font-size:14px;font-weight:700;cursor:pointer}
+  .shot-acts button.adj{border-color:var(--gold);color:var(--gold)}
+  .shot-acts button:disabled{opacity:.35;cursor:default}
   /* both-sides photo strip used by the Preview + Created + Update result views */
   .ph-pair{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin:10px 0 4px}
   .ph-pair figure{margin:0;background:#0f0e0b;border:1px solid var(--line);border-radius:10px;overflow:hidden;cursor:zoom-in}
@@ -446,7 +452,7 @@
   .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{position:fixed;inset:0;z-index:150}
   .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%}
@@ -455,10 +461,10 @@
   .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-presets{position:fixed;left:50%;top:max(14px,env(safe-area-inset-top));transform:translateX(-50%);display:flex;gap:7px;z-index:121}
+  .crop-presets{position:fixed;left:50%;top:max(14px,env(safe-area-inset-top));transform:translateX(-50%);display:flex;gap:7px;z-index:151}
   .crop-presets button{background:rgba(8,7,6,.75);border:1px solid var(--line);color:var(--ink);border-radius:99px;padding:8px 13px;font-size:13px;font-weight:600;cursor:pointer}
   .crop-presets button.on{background:var(--gold);color:#1b1407;border-color:var(--gold)}
-  .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{position:fixed;left:50%;bottom:max(24px,env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;gap:12px;z-index:151}
   .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}
@@ -693,6 +699,10 @@
       <button class="shot-btn" id="frontBtn"><img class="shot-img" id="frontImg" alt="Front photo" hidden><span class="shot-num">1</span><span class="shot-ico">📷</span><b>Front</b><small>pattern face</small><i class="shot-ck" id="frontCk"></i></button>
       <button class="shot-btn" id="backBtn"><img class="shot-img" id="backImg" alt="Back photo" hidden><span class="shot-num">2</span><span class="shot-ico">🏷</span><b>Back</b><small>label / SKU</small><i class="shot-ck" id="backCk"></i></button>
     </div>
+    <div class="shot-acts">
+      <div><button class="adj" id="frontAdj" disabled>✎ Adjust</button><button id="frontRetake" disabled>↻ Retake</button></div>
+      <div><button class="adj" id="backAdj" disabled>✎ Adjust</button><button id="backRetake" disabled>↻ Retake</button></div>
+    </div>
     <div class="shot-status" id="shotStatus"></div>
     <!-- OCR fallback: when the mfr# can't be auto-determined, offer detected code candidates to tap -->
     <div class="cand-pick" id="candPick" hidden></div>
@@ -1279,11 +1289,11 @@ async function uploadPhoto(x,dataUrl){
 /* ===================== Photo editor ===================== */
 let ES=null, edRAF=0;
 const WHEEL_R=64; // half of 128
-function openEditor(x,dataUrl){
+function openEditor(x,dataUrl,onUse){
   const img=new Image();
   img.onload=()=>{
     const flat=document.createElement('canvas'); flat.width=img.width; flat.height=img.height;
-    ES={x,img,flat,b:1,c:1,s:1,h:0,th:0,ts:0,rollMode:'off',curl:0.5};
+    ES={x,img,flat,b:1,c:1,s:1,h:0,th:0,ts:0,rollMode:'off',curl:0.5,onUse:onUse||null};
     const cv=$('#edcanvas'); cv.width=img.width; cv.height=img.height;
     $('#ed-sku').textContent=x.dw_sku||'';
     // reset controls
@@ -1547,7 +1557,7 @@ $('#edUse').addEventListener('click',()=>{
   let dataUrl;
   if(ES.rollMode==='tube'||ES.rollMode==='sheet'){ edRedraw(); dataUrl=$('#edcanvas').toDataURL('image/jpeg',0.9); }
   else { dataUrl=bakeAdjust(ES.img,ES.img.width,ES.img.height).toDataURL('image/jpeg',0.9); }
-  closeEditor(); uploadPhoto(x,dataUrl);
+  const onUse=ES.onUse; closeEditor(); if(onUse) onUse(dataUrl); else uploadPhoto(x,dataUrl);
 });
 async function skip(x){await fetch('/api/skip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dw_sku:x.dw_sku,skipped:true})});x.skipped=true;render();}
 
@@ -2142,7 +2152,9 @@ function renderShotTiles(){
   $('#backBtn').classList.toggle('have',!!_backPhoto); $('#backBtn').classList.toggle('need',!!_frontPhoto && !_backPhoto);
   // TK-12228: show the actual captured photo inside its tile (immediate preview of FRONT and BACK)
   [['#frontImg',_frontPhoto],['#backImg',_backPhoto]].forEach(([id,src])=>{ const im=$(id); if(!im) return;
-    if(src){ if(im.getAttribute('src')!==src) im.src=src; im.hidden=false; } else { im.removeAttribute('src'); im.hidden=true; } }); }
+    if(src){ if(im.getAttribute('src')!==src) im.src=src; im.hidden=false; } else { im.removeAttribute('src'); im.hidden=true; } });
+  $('#frontAdj').disabled=$('#frontRetake').disabled=!_frontPhoto;
+  $('#backAdj').disabled=$('#backRetake').disabled=!_backPhoto; }
 // ── TK-12228: both-sides photo strip for the Preview / Created / Update result views + tap-to-enlarge ──
 const SIDE_LBL={front:'Front · pattern',back:'Back · label',extra:'Extra',photo:'Photo'};
 function escAttr(v){ return String(v==null?'':v).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;'); }
@@ -2742,8 +2754,20 @@ $('#addVendor').addEventListener('change',onVendorPick);
 $('#stickyVendor')&&$('#stickyVendor').addEventListener('change',onStickyPick);
 // Two-photo capture wiring: dedicated FRONT (pattern) + BACK (label) tiles → the in-page live view.
 // Order is enforced: the BACK tile refuses to open the camera until Photo 1 (front) exists.
-$('#frontBtn').addEventListener('click',()=>openTwoShotCam('front'));
-$('#backBtn').addEventListener('click',()=>{ if(!_frontPhoto){ toast('📷 Take Photo 1 (the FRONT) first'); return; } openTwoShotCam('back'); });
+// TK-12256: once a side has a photo, tapping it (or ✎ Adjust) opens the Enhance/Crop editor on that
+// shot; ↻ Retake reopens the camera. Adjusted pixels replace the shot locally — nothing uploads here.
+function adjustShot(side){ const src=side==='front'?_frontPhoto:_backPhoto; if(!src) return;
+  openEditor({dw_sku:side==='front'?'FRONT · pattern':'BACK · label'},src,url=>{
+    if(side==='front'){ _frontPhoto=url; _addPhoto=url; detectAndShowColor(); }
+    else { _backPhoto=url; if(_addPhoto===src) _addPhoto=url; }
+    renderShots(); saveCaptureState(); toast('✓ '+(side==='front'?'Front':'Back')+' photo adjusted'); }); }
+function retakeShot(side){ if(side==='back' && !_frontPhoto){ toast('📷 Take Photo 1 (the FRONT) first'); return; } openTwoShotCam(side); }
+$('#frontBtn').addEventListener('click',()=>{ if(_frontPhoto) adjustShot('front'); else openTwoShotCam('front'); });
+$('#backBtn').addEventListener('click',()=>{ if(_backPhoto){ adjustShot('back'); return; } if(!_frontPhoto){ toast('📷 Take Photo 1 (the FRONT) first'); return; } openTwoShotCam('back'); });
+$('#frontAdj').addEventListener('click',()=>adjustShot('front'));
+$('#backAdj').addEventListener('click',()=>adjustShot('back'));
+$('#frontRetake').addEventListener('click',()=>retakeShot('front'));
+$('#backRetake').addEventListener('click',()=>retakeShot('back'));
 // frontInput/backInput remain as the NATIVE fallback (HTTP / getUserMedia-unavailable) — shotAdd applies WB there.
 $('#frontInput').addEventListener('change',e=>{ const f=e.target.files[0]; e.target.value=''; shotAdd('front',f); });
 $('#backInput').addEventListener('change',e=>{ const f=e.target.files[0]; e.target.value=''; shotAdd('back',f); });

← a2e8198 5x sweep 5: report — sweeps converge at cap, 3 app defects f  ·  back to Dw Photo Capture  ·  Editor fits iPhone width: narrow-screen layout (wheel above 7aecde5 →