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