← back to Dw Photo Capture
public/themes/v5-swipe-deck.html
310 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>v5 — Swipe Deck</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root{ --blush:#F4EDE7; --green:#3FA66B; --red:#C0503F; --gold:#C9A24B; --ink:#2B2B2B; }
*{box-sizing:border-box; touch-action:pan-y;}
html,body{margin:0;padding:0;background:#ddd;font-family:'Inter',sans-serif;color:var(--ink);}
body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
.phone{
width:390px; min-height:780px; background:var(--blush); border-radius:36px; overflow:hidden;
box-shadow:0 24px 60px rgba(0,0,0,.28); display:flex; flex-direction:column; position:relative;
}
header{ padding:16px 20px 10px; text-align:center; }
header .mark{ font-family:'Fraunces',serif; font-weight:700; font-size:18px; color:#7A5C1F; }
header{ position:relative; }
header .homebtn{ position:absolute; right:14px; top:8px; min-width:44px; min-height:44px; padding:0 12px; border-radius:12px;
border:2px solid #d9cfc4; background:#fff; color:var(--ink); font:700 14px Inter,sans-serif; cursor:pointer; }
header .homebtn[hidden]{ display:none; }
header .homebtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
.screen-area{ flex:1; position:relative; display:flex; align-items:center; justify-content:center; padding:14px 18px; overflow:hidden; }
.inner{ width:100%; display:flex; flex-direction:column; gap:14px; }
h1.title{ font-family:'Fraunces',serif; font-weight:700; font-size:24px; margin:0 0 4px; text-align:center; }
p.sub{ margin:0; font-size:17px; color:#555; text-align:center; }
.card{
background:#fff; border-radius:22px; padding:20px; box-shadow:0 10px 28px rgba(0,0,0,.14);
display:flex; flex-direction:column; gap:12px; user-select:none;
}
.card.swiper{ cursor:grab; will-change:transform, opacity; }
.card.swiper:active{ cursor:grabbing; }
.itemname{ font-size:20px; font-weight:800; margin:0; }
.itemmeta{ font-size:15px; color:#666; margin:0; }
.swatch{ width:100%; height:170px; border-radius:16px; }
.sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#7c9c78 18px 36px);}
.sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#617a99 16px 32px);}
.sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#c98f2c 14px 28px);}
.camview{
height:280px; background:#1c1c1e; border-radius:20px; position:relative;
display:flex; align-items:center; justify-content:center; color:#eee;
}
.camview .hint{position:absolute; top:14px; left:0; right:0; text-align:center; font-size:15px; color:#f0e8d8;}
.camview .status{ font-size:19px; font-weight:700; }
.shutter{
position:absolute; bottom:18px; left:50%; transform:translateX(-50%);
width:74px; height:74px; border-radius:50%; background:var(--gold); border:5px solid #fff; font-size:28px;
display:flex; align-items:center; justify-content:center;
}
button{ font-family:'Inter',sans-serif; cursor:pointer; }
.btn{ min-height:56px; width:100%; border-radius:18px; font-size:18px; font-weight:700; border:none;
display:flex; align-items:center; justify-content:center; gap:8px; padding:0 16px; }
.btn-primary{ background:var(--gold); color:#3a2a06; }
.btn-secondary{ background:#fff; color:var(--ink); border:2px solid #ddd; }
.btn:focus-visible, .brandcard:focus-visible, .tabbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
.row{ display:flex; gap:12px; }
.row .btn{ flex:1; }
.swipebtns{ display:flex; justify-content:center; gap:36px; margin-top:6px; }
.circlewrap{ display:flex; flex-direction:column; align-items:center; gap:6px; }
.circle{
width:64px; height:64px; border-radius:50%; border:none; font-size:26px; font-weight:800;
display:flex; align-items:center; justify-content:center; box-shadow:0 6px 14px rgba(0,0,0,.15);
}
.circle.reject{ background:#fff; color:var(--red); border:3px solid var(--red); }
.circle.accept{ background:#fff; color:var(--green); border:3px solid var(--green); }
.circle-label{ font-size:14px; font-weight:700; color:#555; }
.brandgrid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.brandcard{ min-height:60px; border-radius:14px; background:#fff; border:2px solid #e6dccb; font-size:16px; font-weight:700; }
.resultlabel{ position:absolute; top:10px; left:10px; background:rgba(0,0,0,.55); color:#fff; font-size:13px; padding:4px 10px; border-radius:999px; }
.bottombar{
display:flex; justify-content:space-around; align-items:center; padding:10px 6px 16px;
background:#fff; border-top:1px solid #eee;
}
.tabbtn{
background:none; border:none; display:flex; flex-direction:column; align-items:center; gap:4px;
min-width:64px; min-height:56px; font-size:12px; font-weight:700; color:#5c5c5c;
}
.tabbtn .ic{ font-size:24px; }
.tabbtn.active{ color:#7A5C1F; }
</style>
<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
</style>
</head>
<body>
<div class="phone">
<header><span class="mark">DW · Photo</span><button class="homebtn" id="homeBtnTop" type="button" onclick="state.job=null; go('home')" hidden>⌂ Home</button></header>
<div class="screen-area" id="screen"></div>
<div class="bottombar">
<button class="tabbtn" onclick="startJob('new')" id="tab-new"><span class="ic">🆕</span>New</button>
<button class="tabbtn" onclick="startJob('update')" id="tab-update"><span class="ic">🔄</span>Update</button>
<button class="tabbtn" onclick="startJob('find')" id="tab-find"><span class="ic">🔍</span>Find</button>
<button class="tabbtn" onclick="startJob('sample')" id="tab-sample"><span class="ic">📦</span>Sample</button>
</div>
</div>
<script>
const MOCK_ITEMS = [
{name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
{name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2'},
{name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
];
const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0 };
let currentSwiper = null;
function go(screen){ state.screen = screen; render(); }
function startJob(job){ state.job = job; state.item=null; state.brand=null; state.resultIndex=0;
const map = {new:'new-cam', update:'update-scan', find:'find-method', sample:'sample-scan'};
go(map[job]);
}
function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
function shoot(cb){
const st = document.getElementById('camStatus');
const btn = document.querySelector('.shutter');
if(btn) btn.disabled = true;
if(st) st.textContent = 'Snapping…';
const from = state.screen; setTimeout(()=>{ if (state.screen !== from) return; state.item = randomItem(); cb(); }, 700);
}
function swipeButtons(rejectAria, acceptAria){
acceptAria = acceptAria || 'Yes';
return `<div class="swipebtns">
<div class="circlewrap">
<button class="circle reject" aria-label="${rejectAria}" onclick="currentSwiper.swipeLeft()">✗</button>
<span class="circle-label">Redo</span>
</div>
<div class="circlewrap">
<button class="circle accept" aria-label="${acceptAria}" onclick="currentSwiper.swipeRight()">✓</button>
<span class="circle-label">Yes</span>
</div>
</div>`;
}
function cameraCard(hint, onShoot){
return `<div class="camview">
<div class="hint">${hint}</div>
<div class="status" id="camStatus">Tap the shutter</div>
<button class="shutter" aria-label="Snap It" onclick="${onShoot}">📸</button>
</div>`;
}
// generic pointer-drag swipe; buttons call swipeRight()/swipeLeft() on the returned handle
function attachSwipe(el, onRight, onLeft){
let startX=0, dragging=false, dx=0;
function down(e){ dragging=true; startX=e.clientX; el.style.transition='none'; el.setPointerCapture && el.setPointerCapture(e.pointerId); }
function move(e){ if(!dragging) return; dx = e.clientX-startX;
el.style.transform = `translateX(${dx}px) rotate(${dx/18}deg)`;
el.style.opacity = 1 - Math.min(Math.abs(dx)/450,.5);
}
function up(){ if(!dragging) return; dragging=false;
el.style.transition='transform .25s ease, opacity .25s ease';
if(dx>90) flyOut(1,onRight); else if(dx<-90) flyOut(-1,onLeft); else { el.style.transform=''; el.style.opacity=1; }
dx=0;
}
function flyOut(dir,cb){
el.style.transform = `translateX(${dir*520}px) rotate(${dir*28}deg)`;
el.style.opacity=0;
setTimeout(()=>{ if(cb) cb(); }, 200);
}
el.addEventListener('pointerdown', down);
el.addEventListener('pointermove', move);
el.addEventListener('pointerup', up);
el.addEventListener('pointercancel', up);
const handle = { swipeRight:()=>flyOut(1,onRight), swipeLeft:()=>flyOut(-1,onLeft) };
return handle;
}
function render(){
const s = document.getElementById('screen');
document.getElementById('homeBtnTop').hidden = (state.screen === 'home');
['new','update','find','sample'].forEach(j=>{
document.getElementById('tab-'+j).classList.toggle('active', state.job===j);
});
let html = '<div class="inner">';
if(state.screen==='home'){
html += `<div class="card" style="text-align:center; align-items:center;">
<h1 class="title">Ready when you are</h1>
<p class="sub">Pick a job below to get started.</p>
</div>`;
}
else if(state.screen==='new-cam'){
html += `<h1 class="title">Add New Item</h1>` + cameraCard('Take a photo of the sample', "shoot(()=>go('new-review'))");
}
else if(state.screen==='new-review'){
html += `<p class="sub">Swipe or tap to decide</p>`;
html += `<div class="card swiper" id="swipeCard">
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">code ${state.item.code}</p>
</div>`;
html += swipeButtons('Retake', 'Looks Good');
}
else if(state.screen==='new-brand'){
html += `<h1 class="title">Which Brand?</h1><div class="brandgrid">`;
BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('new-savecheck')">${b}</button>`; });
html += `</div>`;
}
else if(state.screen==='new-savecheck'){
html += `<div class="card">
<h1 class="title" style="font-size:20px;">Save this?</h1>
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">${state.brand} · code ${state.item.code}</p>
</div>`;
html += `<button class="btn btn-primary" onclick="go('new-done')">Save as Draft</button>`;
}
else if(state.screen==='new-done'){
html += `<div class="card" style="text-align:center;"><h1 class="title">Saved! 🎉</h1><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p></div>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('new')">Take Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
}
else if(state.screen==='update-scan'){
html += `<h1 class="title">Update an Item</h1>` + cameraCard('Show me the label or pattern', "shoot(()=>go('update-match'))");
}
else if(state.screen==='update-match'){
html += `<p class="sub">Is this it?</p>`;
html += `<div class="card swiper" id="swipeCard">
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">code ${state.item.code}</p>
</div>`;
html += swipeButtons('Not it', "Yes, that's it");
}
else if(state.screen==='update-newphoto'){
html += `<h1 class="title">New Photo</h1>` + cameraCard('Take the new photo', "shoot(()=>go('update-review'))");
}
else if(state.screen==='update-review'){
html += `<p class="sub">Swipe right to confirm</p>`;
html += `<div class="card swiper" id="swipeCard">
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">code ${state.item.code}</p>
</div>`;
html += swipeButtons('Retake');
}
else if(state.screen==='update-done'){
html += `<div class="card" style="text-align:center;"><h1 class="title">Updated! ✅</h1><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p></div>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('update')">Take Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
}
else if(state.screen==='find-method'){
html += `<h1 class="title">Find an Item</h1>`;
html += `<button class="btn btn-secondary" onclick="go('find-stack')">Scan the Label</button>`;
html += `<button class="btn btn-secondary" onclick="go('find-stack')">Say the Name</button>`;
html += `<button class="btn btn-secondary" onclick="go('find-stack')">Type It</button>`;
}
else if(state.screen==='find-stack'){
const it = MOCK_ITEMS[state.resultIndex % MOCK_ITEMS.length];
html += `<p class="sub">Swipe ✗ to skip, ✓ or tap to pick</p>`;
html += `<div class="card swiper" id="swipeCard" onclick="pickResult()" style="position:relative;">
<span class="resultlabel">${(state.resultIndex%MOCK_ITEMS.length)+1} of ${MOCK_ITEMS.length}</span>
<div class="swatch ${it.sw}"></div>
<p class="itemname">${it.name} — ${it.color}</p>
<p class="itemmeta">code ${it.code}</p>
</div>`;
html += swipeButtons('Skip', 'Pick this one');
}
else if(state.screen==='find-detail'){
html += `<div class="card" style="text-align:center;"><h1 class="title" style="font-size:20px;">${state.item.name}</h1><div class="swatch ${state.item.sw}"></div><p class="itemmeta">${state.item.color} · code ${state.item.code}</p></div>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="go('find-stack')">Back</button><button class="btn btn-primary" onclick="go('update-newphoto')">Update This</button></div>`;
}
else if(state.screen==='sample-scan'){
html += `<h1 class="title">Sample Arrived</h1>` + cameraCard('Scan the label on the box', "shoot(()=>go('sample-match'))");
}
else if(state.screen==='sample-match'){
html += `<p class="sub">Mark Received?</p>`;
html += `<div class="card swiper" id="swipeCard">
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">code ${state.item.code}</p>
</div>`;
html += swipeButtons('Not it, rescan', 'Mark Received');
}
else if(state.screen==='sample-printing'){
html += `<div class="card" style="text-align:center;"><h1 class="title" style="font-size:20px;">Printing sticker…</h1><p class="sub">🖨️ One sec…</p></div>`;
}
else if(state.screen==='sample-done'){
html += `<div class="card" style="text-align:center;"><h1 class="title">Done! ✅</h1><p class="sub">Sticker printed and item received.</p></div>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('sample')">Take Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
}
html += '</div>';
s.innerHTML = html;
const swipeEl = document.getElementById('swipeCard');
if(swipeEl){
if(state.screen==='new-review'){ currentSwiper = attachSwipe(swipeEl, ()=>go('new-brand'), ()=>go('new-cam')); }
else if(state.screen==='update-match'){ currentSwiper = attachSwipe(swipeEl, ()=>go('update-newphoto'), ()=>go('update-scan')); }
else if(state.screen==='update-review'){ currentSwiper = attachSwipe(swipeEl, ()=>go('update-done'), ()=>go('update-newphoto')); }
else if(state.screen==='find-stack'){ currentSwiper = attachSwipe(swipeEl, pickResult, skipResult); }
else if(state.screen==='sample-match'){ currentSwiper = attachSwipe(swipeEl, markReceived, ()=>go('sample-scan')); }
}
}
function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex % MOCK_ITEMS.length]; go('find-detail'); }
function skipResult(){ state.resultIndex++; go('find-stack'); }
function markReceived(){ go('sample-printing'); setTimeout(()=>{ if (state.screen === 'sample-printing') go('sample-done'); }, 900); }
render();
</script>
</body>
</html>