[object Object]

← back to Dw Photo Capture

Batch UI: redo as a simple 1-2-3 wizard with plain-English explanations

ac3ec66c28195a77b347ecf15d8b5467c8358993 · 2026-09-18 14:59:35 -0700 · Steve Abrams

Reskin + copy only — all capture logic, element IDs, and CSS class-hooks
unchanged (verified: 53 JS-referenced ids present, both script blocks parse
clean). Setup shows the whole 1·2·3 flow up front with friendly field labels;
Calibrate and Shoot get numbered STEP badges and plain instructions; the
technical chips + debug numbers move behind an ⓘ toggle (still updated by JS);
gate/retake/recal states now read in plain English.

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

Files touched

Diff

commit ac3ec66c28195a77b347ecf15d8b5467c8358993
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 18 14:59:35 2026 -0700

    Batch UI: redo as a simple 1-2-3 wizard with plain-English explanations
    
    Reskin + copy only — all capture logic, element IDs, and CSS class-hooks
    unchanged (verified: 53 JS-referenced ids present, both script blocks parse
    clean). Setup shows the whole 1·2·3 flow up front with friendly field labels;
    Calibrate and Shoot get numbered STEP badges and plain instructions; the
    technical chips + debug numbers move behind an ⓘ toggle (still updated by JS);
    gate/retake/recal states now read in plain English.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Uns3k4K15WBcFJNr3GcUd3
---
 public/batch.html | 98 ++++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 65 insertions(+), 33 deletions(-)

diff --git a/public/batch.html b/public/batch.html
index c95c6ca..b9890ae 100644
--- a/public/batch.html
+++ b/public/batch.html
@@ -79,7 +79,7 @@
   .shutter{width:78px;height:78px;border-radius:50%;border:4px solid var(--gold);background:var(--gold);cursor:pointer;flex:0 0 auto}
   .shutter:disabled{background:#3a3428;border-color:#3a3428}
   .thumb{width:56px;height:56px;border-radius:10px;border:1px solid var(--line);object-fit:cover;background:#16140f}
-  .state{font:800 15px/1 "SF Pro Display",sans-serif;letter-spacing:.06em;text-transform:uppercase;min-height:18px;text-align:center}
+  .state{font:800 20px/1.2 "SF Pro Display",sans-serif;letter-spacing:.01em;min-height:26px;text-align:center;padding:0 10px}
   .state.ready{color:var(--green)} .state.wait{color:var(--muted)} .state.retake{color:var(--red)}
   /* ── big RETAKE / re-cal overlay ── */
   .big{position:absolute;inset:0;z-index:55;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center;padding:24px;background:#0f0e0cd8}
@@ -96,6 +96,24 @@
   .toast{position:absolute;left:50%;bottom:170px;transform:translateX(-50%);background:#16140f;border:1px solid var(--gold);color:var(--ink);padding:10px 16px;border-radius:12px;font-size:14px;z-index:60;opacity:0;transition:opacity .2s;pointer-events:none;max-width:88%;text-align:center}
   .toast.show{opacity:1}
   .qmeta{font:600 10px/1.4 ui-monospace,Menlo,monospace;color:var(--muted);text-align:center;white-space:pre}
+  /* ── 1·2·3 wizard chrome (added: simple, explained flow) ── */
+  .stepbadge{display:flex;align-items:center;gap:10px;margin:2px 0 6px}
+  .stepbadge .num{width:38px;height:38px;border-radius:50%;background:var(--gold);color:#1b1407;font:900 20px/38px "SF Pro Display",sans-serif;text-align:center;flex:0 0 auto}
+  .stepbadge .lbl{font:800 16px/1.1 "SF Pro Display",sans-serif;letter-spacing:.02em;color:var(--gold)}
+  .explain{font-size:15px;line-height:1.5;color:var(--ink);background:#16140f;border:1px solid var(--line);border-left:3px solid var(--gold);border-radius:10px;padding:12px 14px;margin:6px 0 16px}
+  .explain b{color:var(--gold)}
+  .steps3{display:flex;gap:6px;margin:12px 0 18px}
+  .steps3 .s{flex:1;text-align:center;font:700 11px/1.3 -apple-system,sans-serif;color:var(--muted);background:#16140f;border:1px solid var(--line);border-radius:10px;padding:9px 4px}
+  .steps3 .s b{display:block;font:900 17px/1.2 "SF Pro Display",sans-serif;color:var(--gold)}
+  .foot .btn.primary,#vCalib .foot .btn{font-size:17px;padding:16px 24px}
+  /* the "nerd panel" — technical chips + debug numbers, hidden until you tap ⓘ */
+  .detBtn{position:absolute;top:max(10px,env(safe-area-inset-top));right:12px;z-index:33;width:36px;height:36px;border-radius:50%;
+          border:1px solid var(--line);background:#16140fcc;color:var(--muted);font:700 16px/1 sans-serif;backdrop-filter:blur(6px)}
+  #details[hidden]{display:none}
+  /* on-camera instruction card (calibrate) */
+  .instr{font-size:16px;line-height:1.45;color:var(--ink);text-align:center;max-width:440px;margin:0 auto;
+         background:#16140fcc;border:1px solid var(--line);border-radius:12px;padding:12px 14px;backdrop-filter:blur(6px)}
+  .instr b{color:var(--gold)}
 </style>
 </head>
 <body>
@@ -103,19 +121,25 @@
 <!-- ══════════ VIEW 1 · SESSION SETUP ══════════ -->
 <div class="view" id="vSetup">
   <div class="pad">
-    <h1><b>DW</b> Batch Shoot <span class="ver">__VER__</span></h1>
-    <p class="sub">Production-line capture — calibrate once, shoot the whole line. Fixed stand + fixed light.</p>
-    <div class="note">iPad Safari can't hardware-lock white-balance / exposure / focus, so this app <b>software-locks</b>: it stores a calibration frame and checks every shot against it, auto-pausing on drift. No RAW/DNG in a web app — masters are color-corrected JPEGs. Keep the lights fixed and the iPad plugged in.</div>
-    <label class="fld"><span>Vendor</span><select id="fVendorSel"><option value="">— pick vendor —</option><option value="__other__">Other (type it)…</option></select></label>
-    <label class="fld" id="fVendorOtherWrap" hidden><span>Vendor (typed)</span><input id="fVendor" placeholder="Phillip Jeffries" autocapitalize="words"></label>
-    <label class="fld"><span>Collection</span><input id="fCollection" placeholder="Grasscloth" autocapitalize="words"></label>
-    <label class="fld"><span>Date</span><input id="fDate" type="date"></label>
-    <label class="fld"><span>Target count (for the N / count display)</span><input id="fCount" type="number" inputmode="numeric" value="425" min="1"></label>
+    <h1>📸 <b>Sample</b> Photo Booth <span class="ver">__VER__</span></h1>
+    <p class="sub">Take one perfect photo of every sample — the app does the hard part.</p>
+    <div class="explain">Here's the whole job. You'll do it <b>three times only</b>: set up once, calibrate once, then photograph every sample.</div>
+    <div class="steps3">
+      <div class="s"><b>1</b>Calibrate<br>the color</div>
+      <div class="s"><b>2</b>Photograph<br>each sample</div>
+      <div class="s"><b>3</b>Done —<br>all saved</div>
+    </div>
+    <label class="fld"><span>Who made these? (pick the brand)</span><select id="fVendorSel"><option value="">— pick a brand —</option><option value="__other__">Not listed — type it…</option></select></label>
+    <label class="fld" id="fVendorOtherWrap" hidden><span>Brand name (type it)</span><input id="fVendor" placeholder="Phillip Jeffries" autocapitalize="words"></label>
+    <label class="fld"><span>What are they called?</span><input id="fCollection" placeholder="Grasscloth" autocapitalize="words"></label>
+    <label class="fld"><span>Today's date</span><input id="fDate" type="date"></label>
+    <label class="fld"><span>About how many samples?</span><input id="fCount" type="number" inputmode="numeric" value="425" min="1"></label>
     <div class="row" style="justify-content:space-between;margin-top:8px">
-      <button class="btn ghost" id="bResume" hidden>↩ Resume last batch</button>
-      <button class="btn primary" id="bStart" style="margin-left:auto">Start batch →</button>
+      <button class="btn ghost" id="bResume" hidden>↩ Pick up where I left off</button>
+      <button class="btn primary" id="bStart" style="margin-left:auto;font-size:17px;padding:16px 26px">Start → Step 1</button>
     </div>
-    <p class="qmeta" id="setupQ" style="margin-top:16px;text-align:left"></p>
+    <p class="sub" style="margin:14px 0 0;font-size:12px">Keep the light and the stand still the whole time, and keep the iPad plugged in.</p>
+    <p class="qmeta" id="setupQ" style="margin-top:12px;text-align:left"></p>
   </div>
 </div>
 
@@ -123,18 +147,19 @@
 <div class="view" id="vCalib" hidden>
   <div id="cstage"><video id="cv" playsinline autoplay muted></video><canvas class="ov" id="cov"></canvas></div>
   <div id="cflash" class="flash"></div>
-  <div class="bar"><span class="title"><b>Calibrate</b> · gray card</span></div>
+  <div class="bar"><span class="title"><b>STEP 1</b> · Calibrate the color</span></div>
   <div class="foot">
-    <div class="state wait" id="cState">Lay a NEUTRAL GRAY CARD flat, filling the stand. Sample-fill box = green, gray patch = the small square.</div>
+    <div class="instr">Lay the <b>gray card</b> flat so it fills the <b>green box</b>. Hold still, then tap the gold button. This teaches the camera true color so every photo matches.</div>
+    <div class="state wait" id="cState">Put the gray card in the green box</div>
     <div class="row">
       <button class="btn ghost" id="cBack">← Back</button>
-      <button class="btn primary" id="cSave">✓ Capture calibration</button>
+      <button class="btn primary" id="cSave">✓ Capture the gray card</button>
     </div>
   </div>
   <div class="overlay" id="cGate">
-    <h2>Start the camera</h2>
-    <p>Tap to allow camera access, then point at the copy-stand.</p>
-    <button class="btn primary" id="cStartCam">📷 Start Camera</button>
+    <h2>Turn on the camera</h2>
+    <p>Tap the button, then allow the camera. Point it straight down at the stand.</p>
+    <button class="btn primary" id="cStartCam">📷 Turn on camera</button>
   </div>
 </div>
 
@@ -144,10 +169,11 @@
   <div id="flash"></div>
 
   <div class="bar">
-    <span class="title">BATCH: <b id="hBatch">—</b></span>
+    <span class="title"><b>STEP 2</b> · <span id="hBatch">—</span></span>
     <span class="count"><span id="hN">0</span> / <span id="hTotal">—</span></span>
   </div>
-  <div class="chips">
+  <!-- technical readouts — hidden until you tap ⓘ; JS keeps updating them either way -->
+  <div class="chips" id="details" hidden>
     <span class="chip"><span class="d" id="dCam"></span>CAM</span>
     <span class="chip"><span class="d ok"></span>WB·LOCK<sup style="color:var(--muted)">sw</sup></span>
     <span class="chip"><span class="d ok"></span>EXP·LOCK<sup style="color:var(--muted)">sw</sup></span>
@@ -155,28 +181,29 @@
     <span class="chip"><span class="d" id="dCal"></span>CAL <span id="calTxt">—</span></span>
     <span class="chip"><span class="d" id="dSku"></span>SKU <span id="skuTxt">—</span></span>
     <span class="chip">Δ <span id="deltaTxt">—</span> <span style="color:var(--muted)">ΔE:v1.1</span></span>
+    <span class="chip" id="qmeta" style="white-space:normal;color:var(--muted)"></span>
   </div>
 
   <div class="foot">
-    <div class="qmeta" id="qmeta"></div>
-    <div class="state wait" id="state">WAITING</div>
+    <div class="state wait" id="state">Put a sample in the box</div>
     <div class="skuline">
-      <button class="btn ghost" id="bPrev" title="Previous">◀</button>
-      <input id="skuInput" placeholder="SKU (auto / type)" autocapitalize="characters" autocomplete="off" spellcheck="false">
-      <button class="btn ghost" id="bNext" title="Next SKU">Next ▶</button>
+      <button class="btn ghost" id="bPrev" title="Show last code">◀</button>
+      <input id="skuInput" placeholder="sample code — fills in by itself" autocapitalize="characters" autocomplete="off" spellcheck="false">
+      <button class="btn ghost" id="bNext" title="Next sample">Next ▶</button>
     </div>
     <div class="row">
-      <label class="chip" style="gap:6px"><input type="checkbox" id="autoFire" style="width:auto" checked> hands-free</label>
-      <button class="shutter" id="shutter" title="Capture"></button>
-      <img class="thumb" id="thumb" alt="last" hidden>
+      <button class="btn ghost" id="detBtn" title="Technical details" style="padding:12px 15px">ⓘ</button>
+      <label class="chip" style="gap:6px;font-size:12px"><input type="checkbox" id="autoFire" style="width:auto" checked> Auto-snap</label>
+      <button class="shutter" id="shutter" title="Take photo"></button>
+      <img class="thumb" id="thumb" alt="last photo" hidden>
     </div>
     <div class="qmeta" id="queueTxt"></div>
   </div>
 
   <!-- big overlays -->
   <div class="big retake" id="bigRetake" hidden><h2>RETAKE</h2><p id="retakeWhy">—</p><small id="retakeMeta"></small><button class="btn primary" id="retakeOk">OK</button></div>
-  <div class="big recal" id="bigRecal" hidden><h2>RE-CALIBRATE</h2><p>The scene drifted from the calibration frame (light or stand moved). Re-shoot the gray card to keep color consistent.</p><small id="recalMeta"></small><button class="btn primary" id="recalGo">Re-calibrate</button></div>
-  <div class="overlay" id="gate"><h2>Start the camera</h2><p>Tap to allow camera access. Point at the first sample on the stand.</p><button class="btn primary" id="startCam">📷 Start Camera</button></div>
+  <div class="big recal" id="bigRecal" hidden><h2>CHECK COLOR</h2><p>The light or stand moved, so colors won't match. Let's re-shoot the gray card — it only takes a second.</p><small id="recalMeta"></small><button class="btn primary" id="recalGo">Re-shoot gray card</button></div>
+  <div class="overlay" id="gate"><h2>Turn on the camera</h2><p>Tap the button, then put your first sample on the stand.</p><button class="btn primary" id="startCam">📷 Turn on camera</button></div>
   <div class="toast" id="toast"></div>
 </div>
 
@@ -428,14 +455,14 @@ function gateTick(){
   $('#deltaTxt').textContent = 'L'+expDev.toFixed(0)+' W'+(wbDrift*100).toFixed(0)+'%';
   $('#qmeta').textContent = `blur ${blur.toFixed(0)}  clip ${(clip*100).toFixed(1)}%  exp ${expDev.toFixed(0)}  fill ${tgt.var.toFixed(0)}`;
   // drift → force re-cal (the software "lock")
-  if(drift){ $('#recalMeta').textContent = needRecal ? 'Calibration predates this build — please recalibrate.' : ('Δluma '+expDev.toFixed(0)+' · ΔWB '+(wbDrift*100).toFixed(0)+'%'); $('#bigRecal').hidden=false; setState('wait','RE-CAL'); return; }
+  if(drift){ $('#recalMeta').textContent = needRecal ? 'Calibration predates this build — please recalibrate.' : ('Δluma '+expDev.toFixed(0)+' · ΔWB '+(wbDrift*100).toFixed(0)+'%'); $('#bigRecal').hidden=false; setState('wait','Color check needed'); return; }
   else { $('#bigRecal').hidden=true; }
   // OCR the SKU on stillness (debounced) if no manual sku typed for this scene
   const still = stillCount>=CFG.stableFrames && !moving;
   if(still && !ocrPending && !fails.includes('no sample in frame')){ maybeOcr(g); }
   // ready?
   const ready = still && fails.length===0 && (curSku||$('#skuInput').value.trim());
-  setState(ready?'ready':(fails.length?'retake':'wait'), ready?'READY':(fails.length?('CHECK: '+fails[0]):'WAITING'));
+  setState(ready?'ready':(fails.length?'retake':'wait'), ready?'✓ Ready — hold still':(fails.length?FRIENDLY_FAIL(fails[0]):'Put a sample in the box'));
   $('#shutter').disabled = busy;
   // hands-free auto-fire
   const now=Date.now();
@@ -445,6 +472,11 @@ function gateTick(){
 }
 function setChip(sel,cls){ const d=$(sel); if(d) d.className='d '+cls; }
 function setState(cls,txt){ const s=$('#state'); s.className='state '+(cls==='ready'?'ready':cls==='retake'?'retake':'wait'); s.textContent=txt; }
+// plain-English versions of the quality-gate failure keys (for the big status line)
+const FRIENDLY_FAIL_MSG={ blurry:'Hold steady — it looks blurry', clipping:'Light too harsh — soften it', exposure:'Light changed — check it', 'no sample in frame':'Put a sample in the box' };
+function FRIENDLY_FAIL(k){ return FRIENDLY_FAIL_MSG[k]||('Check: '+k); }
+// ⓘ details toggle — reveal/hide the technical readouts (chips + debug numbers)
+{ const b=$('#detBtn'); if(b) b.onclick=()=>{ const d=$('#details'); if(d) d.hidden=!d.hidden; }; }
 
 // ── OCR the label → SKU (uses existing /api/identify; debounced + scene-cached) ──
 async function maybeOcr(g){
@@ -600,7 +632,7 @@ setInterval(()=>{ if(!$('#vShoot').hidden) drainQueue(); }, 6000);   // retry lo
 window.addEventListener('online', drainQueue);
 
 // setup-view help text
-$('#setupQ').textContent='v1 ships: gray-card WB + exposure normalize · quality gate (blur/clip/exposure/fill) · auto SKU via label OCR · hands-free motion-stop + BT shutter · durable upload queue.\nv1.1: 24-patch ColorChecker CCM + ΔE, Shopify batch-attach, true perspective warp, thermal auto-tune.';
+$('#setupQ').textContent='Tip: with Auto-snap on, just hold each sample still — the app checks the photo is sharp and well-lit, reads the code, and saves it by itself. A red message means fix what it says, then try again.';
 </script>
 </body>
 </html>

← 40f89a1 auto-data-snapshot: 2026-09-18T14:54:24 (2 data files) — dat  ·  back to Dw Photo Capture  ·  auto-data-snapshot: 2026-09-18T15:26:25 (6 data files) — 5x/ caccd25 →