← back to Dw Photo Capture
Photo capture: vendor picker → sticky overlay bar on the live camera
0c8c48f715aaa6dd95861b442d6fb3ab4a7fa6bb · 2026-09-18 13:41:38 -0700 · steve
- Camera is not gated behind vendor selection; XL vendor dropdown floats across
the top of the live camera view (#scanlive) as a sticky overlay bar
- Auto-sticky: last-picked vendor persists in localStorage (dwphoto_vendor/vidsel),
restored on load; every capture inherits the sticky vendor (pick once, keep shooting)
- Modal picker mirrors + inherits the sticky selection (no per-shot re-pick); a
dedup/OCR/voice match updates the sticky vendor. Backend unchanged.
- Verified in headless Chrome: reload restores the vendor; modal inherits it; 0 errors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCq3132eTmJESAKqJBM8FN
Files touched
Diff
commit 0c8c48f715aaa6dd95861b442d6fb3ab4a7fa6bb
Author: steve <steve@designerwallcoverings.com>
Date: Fri Sep 18 13:41:38 2026 -0700
Photo capture: vendor picker → sticky overlay bar on the live camera
- Camera is not gated behind vendor selection; XL vendor dropdown floats across
the top of the live camera view (#scanlive) as a sticky overlay bar
- Auto-sticky: last-picked vendor persists in localStorage (dwphoto_vendor/vidsel),
restored on load; every capture inherits the sticky vendor (pick once, keep shooting)
- Modal picker mirrors + inherits the sticky selection (no per-shot re-pick); a
dedup/OCR/voice match updates the sticky vendor. Backend unchanged.
- Verified in headless Chrome: reload restores the vendor; modal inherits it; 0 errors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCq3132eTmJESAKqJBM8FN
---
public/index.html | 71 +++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 56 insertions(+), 15 deletions(-)
diff --git a/public/index.html b/public/index.html
index 236b432..fe6e41f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -118,6 +118,14 @@
/* live SKU scanner */
.scanlive{position:fixed;inset:0;z-index:110;background:#000;display:flex;align-items:center;justify-content:center}
.scanlive[hidden]{display:none}
+ /* Sticky vendor bar overlaying the TOP of the live camera — XL touch target, auto-persists */
+ .vendor-sticky{position:absolute;top:0;left:0;right:0;z-index:6;padding:calc(env(safe-area-inset-top,0px) + 10px) 12px 12px;
+ background:linear-gradient(180deg,rgba(8,7,6,.92) 0%,rgba(8,7,6,.82) 70%,rgba(8,7,6,0) 100%)}
+ .vendor-sticky .vs-lbl{display:block;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#e0cf9f;font-weight:700;margin-bottom:6px}
+ .vendor-sticky select.xl-vendor{border-color:var(--gold);background:#1b1812e8}
+ .vendor-sticky .vs-meta{margin-top:6px;font-size:13px;color:#cdbf9c;text-shadow:0 1px 2px #000}
+ .vendor-sticky .vs-meta b{color:#f0e2b8}
+ .vendor-sticky select.xl-vendor.need{border-color:#e0483a;box-shadow:0 0 0 3px rgba(224,72,58,.4)}
#scanvideo{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.scan-frame{position:relative;width:78%;max-width:520px;aspect-ratio:5/2;border:2px solid rgba(255,255,255,.85);border-radius:16px;box-shadow:0 0 0 4000px rgba(0,0,0,.45);overflow:hidden}
.scan-line{position:absolute;left:0;right:0;height:2px;background:var(--gold);box-shadow:0 0 10px var(--gold);animation:scan 1.6s ease-in-out infinite}
@@ -385,6 +393,13 @@
<!-- ===== Live SKU scanner: camera auto-locks on a printed SKU, then looks it up ===== -->
<div class="scanlive" id="scanlive" hidden>
<video id="scanvideo" playsinline muted></video>
+ <!-- STICKY VENDOR: floats across the top of the LIVE camera. Pick once ("lock in, then roll") —
+ persists in localStorage; every capture auto-tags to this vendor. Change it by re-opening it. -->
+ <div class="vendor-sticky" id="vendorSticky">
+ <label class="vs-lbl" for="stickyVendor">Shooting for</label>
+ <select id="stickyVendor" class="xl-vendor"><option value="">— pick vendor —</option></select>
+ <div class="vs-meta" id="stickyVendorMeta"></div>
+ </div>
<div class="scan-frame"><div class="scan-line"></div></div>
<div class="scan-status" id="scanStatus">Point at the printed SKU…</div>
<div class="scan-fields" id="scanFields">
@@ -506,7 +521,7 @@
<div class="media-sub" id="addTarget"></div>
<!-- VENDOR FIRST: pick the vendor before anything else — it drives the DW#, Shopify vendor + FM vid -->
<div class="xl-vendor-wrap" id="xlVendorWrap">
- <label class="xl-vendor-lbl">1 · Pick vendor</label>
+ <label class="xl-vendor-lbl">Vendor · sticky <small style="text-transform:none;letter-spacing:0;color:#8f866f;font-weight:400">(set in the camera's top bar — change here only for this one item)</small></label>
<select id="addVendor" class="xl-vendor"><option value="">Loading vendors…</option></select>
<div class="xl-vendor-meta" id="xlVendorMeta"></div>
</div>
@@ -1755,22 +1770,41 @@ async function addMediaAdd(type,file){ if(!file)return; const c=mediaCounts();
if(file.size>150*1024*1024){ toast('Video too big (>150MB)'); return; }
const dataUrl=await fileToDataUrl(file); _media.push({type:'video',dataUrl,name:file.name||'clip.mp4'}); renderMedia(); } }
let _vreg=[];
+// One <option> set, applied to BOTH the sticky camera bar and the (mirrored) modal picker.
+function vendorOptionsHtml(placeholder){ return `<option value="">${placeholder}</option>`+_vreg.map(v=>
+ `<option value="${v.vendor}" data-vid="${v.vid||''}" data-prefix="${v.sku_prefix||''}" data-fmvid="${v.fm_vid||''}" data-start="${v.sku_range_start||0}">${v.vendor}${v.sku_prefix?(' — '+v.sku_prefix):''}</option>`).join(''); }
async function loadVendors(){ if(_vendorsLoaded) return; try{
// VENDOR-FIRST: the canonical vendor_registry drives DW# + Shopify vendor + FM vid.
const r=await(await fetch('/api/vendors-registry')).json(); _vreg=r.vendors||[];
- $('#addVendor').innerHTML='<option value="">— pick vendor first —</option>'+_vreg.map(v=>
- `<option value="${v.vendor}" data-vid="${v.vid||''}" data-prefix="${v.sku_prefix||''}" data-fmvid="${v.fm_vid||''}" data-start="${v.sku_range_start||0}">${v.vendor}${v.sku_prefix?(' — '+v.sku_prefix):''}</option>`).join('');
+ $('#addVendor').innerHTML=vendorOptionsHtml('— pick vendor first —');
+ if($('#stickyVendor')) $('#stickyVendor').innerHTML=vendorOptionsHtml('— pick vendor —');
_vendorsLoaded=true;
+ restoreStickyVendor(); // auto-sticky: bring back the last vendor picked on this device
}catch(e){ /* keep the loading state visible on failure */ } }
-// When a vendor is chosen, auto-fill VID from the registry + show the canonical minting series.
-function onVendorPick(){ const sel=$('#addVendor'); const o=sel.options[sel.selectedIndex]; sel.classList.remove('need');
- if(!o||!o.value){ $('#xlVendorMeta').innerHTML=''; return; }
- const fmvid=o.getAttribute('data-fmvid')||'', vid=o.getAttribute('data-vid')||'', pfx=o.getAttribute('data-prefix')||'';
- if(fmvid) $('#addVid').value=fmvid; else if(vid && !$('#addVid').value) $('#addVid').value='';
- $('#xlVendorMeta').innerHTML = pfx
- ? `Series <b>${pfx.replace(/-+$/,'')}</b> · next DW# minted on Preview${fmvid?(' · FM vid <b>'+fmvid+'</b>'):' · <b>FM vid unresolved</b>'}`
- : `⚠ no SKU prefix in registry — will use a <b>PROVISIONAL</b> SKU (flag before go-live)`;
-}
+// Human-readable minting hint for a chosen <option> (series + FM-vid + provisional warning).
+function vendorMetaHtml(o){ if(!o||!o.value) return '';
+ const fmvid=o.getAttribute('data-fmvid')||'', pfx=o.getAttribute('data-prefix')||'';
+ return pfx ? `Series <b>${pfx.replace(/-+$/,'')}</b> · next DW# on Preview${fmvid?(' · FM vid <b>'+fmvid+'</b>'):' · <b>FM vid unresolved</b>'}`
+ : `⚠ no SKU prefix — will use a <b>PROVISIONAL</b> SKU (flag before go-live)`;
+}
+// AUTO-STICKY: persist the current vendor to localStorage + mirror it into the modal picker.
+// The user picks ONCE in the camera's top bar, then keeps shooting; every capture inherits it.
+function setStickyVendor(val, save){
+ const s=$('#stickyVendor'); if(s && s.value!==val) s.value=val;
+ const o=(s&&s.options[s.selectedIndex])|| [...$('#addVendor').options].find(x=>x.value===val);
+ const vid=o?o.getAttribute('data-vid')||'':'';
+ // mirror to the modal picker so add/preview/voice-fill/dedup all see the same vendor
+ if($('#addVendor')){ $('#addVendor').value=val; $('#addVendor').classList.remove('need'); if(vid) $('#addVid').value=vid; }
+ if($('#stickyVendorMeta')) $('#stickyVendorMeta').innerHTML=vendorMetaHtml(o);
+ if($('#xlVendorMeta')) $('#xlVendorMeta').innerHTML=vendorMetaHtml(o);
+ if(s) s.classList.toggle('need', !val);
+ if(save){ setLS('vendor', val||''); setLS('vidsel', vid||''); }
+}
+function restoreStickyVendor(){ const v=LS('vendor'); if(v && [...$('#stickyVendor').options].some(o=>o.value===v)){ setStickyVendor(v,false); } }
+// Sticky bar changed → becomes the new sticky selection (persisted).
+function onStickyPick(){ const s=$('#stickyVendor'); setStickyVendor(s.value, true); }
+// Modal picker changed (rare — a one-off override) → also update the sticky selection.
+function onVendorPick(){ const val=$('#addVendor').value; setStickyVendor(val, true); }
function openAddModal(pref,opts){ opts=opts||{}; _addMode=opts.mode||'add'; _addSession++; addResetMedia();
['addMfr','addVid','addName','addColor','addPrice'].forEach(id=>$('#'+id).value=''); $('#addNote').textContent=''; $('#addResult').innerHTML='';
$('#addTitle').textContent = _addMode==='update' ? 'Update SKU on Shopify' : 'Add new item';
@@ -1778,8 +1812,13 @@ function openAddModal(pref,opts){ opts=opts||{}; _addMode=opts.mode||'add'; _add
$('#addPreview').textContent = _addMode==='update' ? '⬆ Push media to SKU (LIVE)' : '👁 Preview';
if(pref){ if(pref.mfr)$('#addMfr').value=pref.mfr; if(pref.color)$('#addColor').value=pref.color; if(pref.name)$('#addName').value=pref.name; }
$('#addModal').hidden=false; document.body.style.overflow='hidden';
- $('#xlVendorMeta').innerHTML=''; $('#addVendor').classList.toggle('need', _addMode!=='update');
- loadVendors().then(()=>{ if(pref&&pref.vendor){ $('#addVendor').value=pref.vendor; } onVendorPick(); });
+ $('#xlVendorMeta').innerHTML='';
+ // INHERIT the sticky vendor — no per-shot re-pick. A dedup/OCR pref match becomes the new sticky.
+ loadVendors().then(()=>{
+ const inherit = (pref&&pref.vendor) ? pref.vendor : (LS('vendor')||($('#stickyVendor')&&$('#stickyVendor').value)||'');
+ setStickyVendor(inherit, !!(pref&&pref.vendor));
+ $('#addVendor').classList.toggle('need', _addMode!=='update' && !inherit);
+ });
if(opts.camera) setTimeout(()=>$('#addPhotoInput').click(),250); // onload → go straight to camera
}
function addPhotosList(){ return _media.filter(m=>m.type==='photo').map(m=>m.dataUrl); }
@@ -1901,6 +1940,7 @@ async function addUpdate(){ if(!_updatePid) await addResolveUpdate();
$('#addBtn').addEventListener('click',()=>openAddModal());
$('#addClose').addEventListener('click',()=>{ $('#addModal').hidden=true; document.body.style.overflow=''; });
$('#addVendor').addEventListener('change',onVendorPick);
+$('#stickyVendor')&&$('#stickyVendor').addEventListener('change',onStickyPick);
$('#addPhotoBtn').addEventListener('click',()=>$('#addPhotoInput').click());
$('#addVideoBtn').addEventListener('click',()=>$('#addVideoInput').click());
$('#addPhotoInput').addEventListener('change',e=>{ const files=[...e.target.files]; e.target.value=''; files.forEach(f=>addMediaAdd('photo',f)); });
@@ -2069,6 +2109,7 @@ function syncCamBtn(){ $('#camBtn').textContent=(camFacing==='user'?'🤳 Front'
$('#camBtn').addEventListener('click',flipCam);
$('#micBtn').addEventListener('click',voiceSearch);
$('#scanBtn').addEventListener('click',()=>{
+ loadVendors(); // populate the sticky camera vendor bar + restore the last-picked (auto-sticky) vendor
if(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) openLiveScan();
else $('#scanInput').click(); // HTTP (no secure context) → fall back to single-snap
});
@@ -2091,7 +2132,7 @@ $('#simInput').addEventListener('change',async e=>{
collapsed=false; await recognizePattern(f);
});
applyCamCapture(); syncCamBtn(); // apply saved camera choice to capture inputs + header toggle
-load(); loadNew(); loadFav(); loadRecent(); // NOTE: loadTwil() (36MB/88k) is now lazy — fires on 'skip'/TWIL view, not page-load (iOS memory)
+load(); loadNew(); loadFav(); loadRecent(); loadVendors(); // loadVendors: populate + auto-restore the sticky camera vendor bar on boot. NOTE: loadTwil() (36MB/88k) is lazy — fires on 'skip'/TWIL view, not page-load (iOS memory)
setInterval(()=>{ if(filter!=='any'&&filter!=='shop'&&filter!=='fav'&&filter!=='recent'&&filter!=='similar') load(); }, 60000);
</script>
</body>
← 0a65a03 Photo capture: vendor-first new-SKU creation across Shopify
·
back to Dw Photo Capture
·
batch mode: vendor is now a dropdown (catalog vendor list) i e74d7de →