← back to Dw Photo Capture
public/kid-simple/v11-bottom-sheet.html
398 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>v11 — Bottom Sheet</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,500;6..72,600;6..72,700&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--cream:#FFF8ED; --ink:#241E17; --copper:#B5652D; --copper-ink:#3A200D;
--olive:#6B7660; --forest:#4F8B6B; --line:#EAE0CE; --muted:#8A8074;
--camdark:#15130F;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:#ddd;font-family:'Manrope',sans-serif;color:var(--ink);}
body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
.phone{
width:390px; height:780px; background:var(--camdark); border-radius:36px; overflow:hidden;
box-shadow:0 24px 60px rgba(0,0,0,.32); position:relative; isolation:isolate;
}
/* ---------- camera backdrop (always full-bleed, hero) ---------- */
.camera-bg{
position:absolute; inset:0;
background:
radial-gradient(120% 90% at 50% 8%, #2a251d 0%, #15130f 55%, #0e0c09 100%);
}
.cam-grid{ position:absolute; inset:0; opacity:.16;
background-image:
linear-gradient(rgba(255,255,255,.5) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.5) 1px, transparent 1px);
background-size:33.33% 33.33%;
}
.cam-corner{ position:absolute; width:34px; height:34px; border-color:#F3E7CC; opacity:.85; }
.cam-corner.tl{ top:56px; left:20px; border-top:3px solid; border-left:3px solid; border-radius:6px 0 0 0;}
.cam-corner.tr{ top:56px; right:20px; border-top:3px solid; border-right:3px solid; border-radius:0 6px 0 0;}
.mark{
position:absolute; top:18px; left:20px; color:#F3E7CC; font-family:'Manrope',sans-serif;
font-weight:800; font-size:13px; letter-spacing:.14em; opacity:.92;
}
.live-pill{
position:absolute; top:18px; right:20px; display:flex; align-items:center; gap:6px;
background:rgba(0,0,0,.35); border:1px solid rgba(255,255,255,.25); border-radius:20px;
padding:5px 12px 5px 9px; color:#F3E7CC; font-size:12px; font-weight:700; letter-spacing:.06em;
}
.live-dot{ width:8px; height:8px; border-radius:50%; background:#E2543A; box-shadow:0 0 0 3px rgba(226,84,58,.28); }
.cam-hint{
position:absolute; left:24px; right:24px; top:110px; text-align:center;
font-family:'Newsreader',serif; font-weight:600; font-size:25px; line-height:1.28;
color:#FBF3E3; text-shadow:0 2px 14px rgba(0,0,0,.5);
}
.cam-status{
position:absolute; left:0; right:0; text-align:center; color:#D8CBB0; font-size:14px; font-weight:600;
letter-spacing:.03em;
}
/* shutter floats above the sheet, only present on "shoot" steps */
.shutter{
position:absolute; left:50%; transform:translateX(-50%);
width:78px; height:78px; border-radius:50%; background:var(--copper);
border:5px solid #FBF3E3; display:flex; align-items:center; justify-content:center;
font-size:28px; box-shadow:0 8px 22px rgba(0,0,0,.4); z-index:5;
}
.shutter:focus-visible{ outline:3px solid #2B6CB0; outline-offset:3px; }
.shutter[disabled]{ opacity:.55; }
/* ---------- bottom sheet ---------- */
.sheet{
position:absolute; left:0; right:0; bottom:0; z-index:10;
background:var(--cream); border-radius:26px 26px 0 0;
box-shadow:0 -12px 30px rgba(0,0,0,.28);
display:flex; flex-direction:column;
transition:height .32s cubic-bezier(.22,.9,.32,1);
height:340px; touch-action:none;
}
.sheet.full{ height:640px; }
.handle-wrap{
display:flex; flex-direction:column; align-items:center; padding:10px 0 4px; cursor:grab; flex:0 0 auto;
}
.handle-bar{ width:44px; height:5px; border-radius:4px; background:#DDD1B8; }
.sheet-body{ flex:1; min-height:0; overflow-y:auto; padding:6px 24px 26px; }
.home-fab{
position:absolute; top:14px; left:14px; z-index:20; min-width:56px; min-height:56px;
border-radius:16px; background:rgba(20,17,12,.55); border:1px solid rgba(255,255,255,.25); color:#FBF3E3;
font-size:14px; font-weight:700; display:none; align-items:center; justify-content:center; gap:4px;
padding:0 12px; backdrop-filter: blur(2px);
}
.home-fab:focus-visible{ outline:3px solid #7EC0E8; outline-offset:2px; }
h1.q{
font-family:'Newsreader',serif; font-weight:600; font-size:26px; line-height:1.2;
margin:8px 0 18px; color:var(--ink); text-align:left;
}
h1.q.center{ text-align:center; }
.sub{ font-size:14px; color:var(--muted); margin:-12px 0 16px; font-weight:600; }
.rows{ display:flex; flex-direction:column; gap:12px; }
button{ font-family:'Manrope',sans-serif; cursor:pointer; }
.row-btn{
width:100%; min-height:64px; border-radius:18px; border:2px solid var(--line); background:#fff;
display:flex; align-items:center; gap:14px; padding:10px 16px; text-align:left;
font-size:17px; font-weight:700; color:var(--ink);
}
.row-btn .ic{ font-size:26px; width:36px; text-align:center; flex:0 0 auto; }
.row-btn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
.bigbtn{
width:100%; min-height:60px; border-radius:16px; font-size:17px; font-weight:800;
background:#fff; border:2px solid var(--line); color:var(--ink); padding:10px 16px;
}
.bigbtn.primary{ background:var(--copper); border-color:var(--copper); color:#FFF6E9; }
.bigbtn.secondary{ background:#fff; border-color:var(--olive); color:var(--olive); }
.bigbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
.linktext{
background:none; border:none; color:var(--muted); font-size:15px; text-decoration:underline;
text-align:center; min-height:56px; width:100%; font-weight:600;
}
.brandlist{ display:flex; flex-direction:column; gap:10px; }
.brandlist .row-btn{ font-size:16px; min-height:58px; }
.swatch{ width:100%; height:150px; border-radius:16px; margin-bottom:12px; }
.swatch.sm{ width:120px; height:120px; margin:0 auto 14px; }
.sw1{background:repeating-linear-gradient(45deg,#B5652D 0 18px,#96501f 18px 36px);}
.sw2{background:repeating-linear-gradient(-45deg,#6B7660 0 16px,#565f4d 16px 32px);}
.sw3{background:repeating-linear-gradient(90deg,#D9A96A 0 14px,#c48f4a 14px 28px);}
.itemname{ font-size:19px; font-weight:800; margin:0; text-align:center; }
.itemmeta{ font-size:14px; color:var(--muted); margin:4px 0 0; text-align:center; font-weight:600; }
.center-wrap{ display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:160px; gap:6px; }
.done-emoji{ font-size:44px; }
</style>
</head>
<body>
<div class="phone" id="phone">
<div class="camera-bg">
<div class="cam-grid"></div>
<div class="mark">DW · CAPTURE</div>
<div class="live-pill"><span class="live-dot"></span>LIVE</div>
<div class="cam-corner tl"></div>
<div class="cam-corner tr"></div>
<div class="cam-hint" id="camHint"></div>
<div class="cam-status" id="camStatus" style="bottom:378px;"></div>
<button class="shutter" id="shutterBtn" style="bottom:364px;" aria-label="Take photo" onclick="handleShoot()">📸</button>
</div>
<button class="home-fab" id="homeFab" aria-label="Home" onclick="go('home')">⌂ Home</button>
<div class="sheet" id="sheet">
<div class="handle-wrap" id="handleWrap">
<div class="handle-bar"></div>
</div>
<div class="sheet-body" id="sheetBody" role="main"></div>
</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 };
// screen -> {sheet: 'peek'|'full', cam:{hint,status,shutter:bool}}
function meta(screen){
const M = {
'home': {sheet:'peek', cam:{hint:'', status:'', shutter:false}},
'new-cam': {sheet:'peek', cam:{hint:'Take a photo of it', status:'Tap the shutter', shutter:true}},
'new-brand': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'new-review': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'new-done': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'update-cam': {sheet:'peek', cam:{hint:'Scan the label or pattern', status:'Tap the shutter', shutter:true}},
'update-match':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
'update-cam2': {sheet:'peek', cam:{hint:'Take the new photo', status:'Tap the shutter', shutter:true}},
'update-done': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'find-method': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'find-result': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'find-exhausted':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
'find-detail': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
'sample-cam': {sheet:'peek', cam:{hint:'Scan the box label', status:'Tap the shutter', shutter:true}},
'sample-match':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
'sample-printing':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
'sample-done': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
};
return M[screen];
}
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-cam', find:'find-method', sample:'sample-cam'};
go(map[job]);
}
function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
let shootTarget = null;
function armShoot(nextScreen){ shootTarget = nextScreen; }
function handleShoot(){
if(!shootTarget) return;
const btn = document.getElementById('shutterBtn');
const st = document.getElementById('camStatus');
btn.disabled = true;
st.textContent = 'Snapping…';
const dest = shootTarget;
setTimeout(()=>{ state.item = randomItem(); go(dest); }, 650);
}
function bodyHTML(){
const s = state.screen;
if(s==='home'){
return `<h1 class="q">What are we doing?</h1>
<p class="sub">Pick one to get started</p>
<div class="rows">
<button class="row-btn" onclick="startJob('new')"><span class="ic">🆕</span>Add New Item</button>
<button class="row-btn" onclick="startJob('update')"><span class="ic">🔄</span>Update an Item</button>
<button class="row-btn" onclick="startJob('find')"><span class="ic">🔍</span>Find an Item</button>
<button class="row-btn" onclick="startJob('sample')"><span class="ic">📦</span>A Sample Came In</button>
</div>`;
}
if(s==='new-cam' || s==='update-cam' || s==='update-cam2' || s==='sample-cam'){
return `<p class="sub" style="margin-top:8px;">Point the camera above, then tap the shutter</p>`;
}
if(s==='new-brand'){
return `<h1 class="q">Which brand is it?</h1><div class="brandlist">` +
BRANDS.map(b=>`<button class="row-btn" onclick="state.brand='${b}'; go('new-review')">${b}</button>`).join('') +
`</div>`;
}
if(s==='new-review'){
return `<h1 class="q center">Save it?</h1>
<div class="swatch sm ${state.item.sw}"></div>
<p class="itemmeta" style="margin-bottom:18px;">${state.brand} · code ${state.item.code}</p>
<div class="rows">
<button class="bigbtn primary" onclick="go('new-done')">Save as Draft</button>
<button class="linktext" onclick="go('new-cam')">Retake Photo</button>
</div>`;
}
if(s==='new-done'){
return `<div class="center-wrap"><div class="done-emoji">🎉</div><h1 class="q center">Saved!</h1></div>
<div class="rows">
<button class="bigbtn primary" onclick="startJob('new')">Add Another</button>
<button class="linktext" onclick="go('home')">Back Home</button>
</div>`;
}
if(s==='update-match'){
return `<h1 class="q center">Is this it?</h1>
<div class="swatch sm ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta" style="margin-bottom:16px;">code ${state.item.code}</p>
<div class="rows">
<button class="bigbtn primary" onclick="go('update-cam2')">Yes</button>
<button class="bigbtn secondary" onclick="go('update-cam')">No, Let Me Search</button>
</div>`;
}
if(s==='update-done'){
return `<div class="center-wrap"><div class="done-emoji">✅</div><h1 class="q center">Updated!</h1></div>
<div class="rows"><button class="bigbtn primary" onclick="go('home')">Back Home</button></div>`;
}
if(s==='find-method'){
return `<h1 class="q">How do you want to find it?</h1>
<div class="rows">
<button class="row-btn" onclick="go('find-result')"><span class="ic">📷</span>Scan the Label</button>
<button class="row-btn" onclick="go('find-result')"><span class="ic">🎤</span>Say the Name</button>
<button class="row-btn" onclick="go('find-result')"><span class="ic">⌨️</span>Type It</button>
</div>`;
}
if(s==='find-result'){
const it = MOCK_ITEMS[state.resultIndex];
return `<h1 class="q center">Which one is it?</h1>
<div class="swatch sm ${it.sw}"></div>
<p class="itemname">${it.name} — ${it.color}</p>
<p class="itemmeta" style="margin-bottom:16px;">code ${it.code}</p>
<div class="rows">
<button class="bigbtn primary" onclick="pickResult()">Yes, This One</button>
<button class="bigbtn secondary" onclick="nextResult()">Next Result</button>
</div>`;
}
if(s==='find-exhausted'){
return `<h1 class="q">That's all — try another way?</h1>
<div class="rows">
<button class="row-btn" onclick="state.resultIndex=0; go('find-result')"><span class="ic">📷</span>Scan the Label</button>
<button class="row-btn" onclick="state.resultIndex=0; go('find-result')"><span class="ic">🎤</span>Say the Name</button>
<button class="row-btn" onclick="state.resultIndex=0; go('find-result')"><span class="ic">⌨️</span>Type It</button>
</div>`;
}
if(s==='find-detail'){
return `<div class="center-wrap">
<h1 class="q center" style="font-size:22px; margin-bottom:12px;">${state.item.name}</h1>
<div class="swatch sm ${state.item.sw}"></div>
<p class="itemmeta" style="margin-bottom:16px;">${state.item.color} · code ${state.item.code}</p>
</div>
<div class="rows">
<button class="bigbtn primary" onclick="go('update-cam2')">Update This</button>
<button class="bigbtn secondary" onclick="go('find-method')">Search Again</button>
<button class="linktext" onclick="go('home')">Home</button>
</div>`;
}
if(s==='sample-match'){
return `<h1 class="q center">Mark it received?</h1>
<div class="swatch sm ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta" style="margin-bottom:16px;">code ${state.item.code}</p>
<div class="rows"><button class="bigbtn primary" onclick="go('sample-printing')">Yes, Mark Received</button></div>`;
}
if(s==='sample-printing'){
setTimeout(()=>{ if(state.screen==='sample-printing') go('sample-done'); }, 1000);
return `<div class="center-wrap"><div class="done-emoji">🖨️</div><h1 class="q center">Printing your sticker…</h1></div>`;
}
if(s==='sample-done'){
return `<div class="center-wrap"><div class="done-emoji">✅</div><h1 class="q center">Done!</h1></div>
<div class="rows"><button class="bigbtn primary" onclick="go('home')">Back Home</button></div>`;
}
return '';
}
function render(){
const m = meta(state.screen);
const sheet = document.getElementById('sheet');
const fab = document.getElementById('homeFab');
const camHint = document.getElementById('camHint');
const camStatus = document.getElementById('camStatus');
const shutterBtn = document.getElementById('shutterBtn');
sheet.classList.toggle('full', m.sheet === 'full');
fab.style.display = state.screen === 'home' ? 'none' : 'flex';
camHint.textContent = m.cam.hint;
camStatus.textContent = m.cam.status;
if(m.cam.shutter){
shutterBtn.style.display = 'flex';
shutterBtn.disabled = false;
const nextMap = {'new-cam':'new-brand', 'update-cam':'update-match', 'update-cam2':'update-done', 'sample-cam':'sample-match'};
armShoot(nextMap[state.screen]);
} else {
shutterBtn.style.display = 'none';
shootTarget = null;
}
document.getElementById('sheetBody').innerHTML = bodyHTML();
}
function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('find-detail'); }
function nextResult(){
if(state.resultIndex < MOCK_ITEMS.length-1){ state.resultIndex++; go('find-result'); }
else { go('find-exhausted'); }
}
/* ---- draggable handle: free-drag between peek(340) and full(640), snaps on release ---- */
(function(){
const sheet = document.getElementById('sheet');
const handle = document.getElementById('handleWrap');
const PEEK = 340, FULL = 640;
let dragging = false, startY = 0, startH = PEEK;
function clamp(v){ return Math.max(PEEK-40, Math.min(FULL+20, v)); }
handle.addEventListener('pointerdown', (e)=>{
dragging = true;
startY = e.clientY;
startH = sheet.getBoundingClientRect().height;
sheet.style.transition = 'none';
handle.setPointerCapture(e.pointerId);
});
handle.addEventListener('pointermove', (e)=>{
if(!dragging) return;
const dy = startY - e.clientY;
sheet.style.height = clamp(startH + dy) + 'px';
});
function endDrag(){
if(!dragging) return;
dragging = false;
sheet.style.transition = '';
const h = sheet.getBoundingClientRect().height;
const snapFull = h > (PEEK+FULL)/2;
sheet.style.height = '';
sheet.classList.toggle('full', snapFull);
}
handle.addEventListener('pointerup', endDrag);
handle.addEventListener('pointercancel', endDrag);
})();
render();
</script>
</body>
</html>