← 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 →