← back to Dw Photo Capture

public/kid-simple/v15-thumb-zone.html

494 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>v15 — Thumb Zone (Shoot-First / One-Thumb Confirm)</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=Spectral:wght@500;600;700&family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
  :root{
    --stage:#07070A;
    --panel:#18120E;
    --panel-edge:#241A13;
    --line:#3A2C20;
    --ink:#F6EFE4;
    --muted:#A9998A;
    --accent:#FF8A42;
    --accent-ink:#2B1206;
    --success:#5FCE8B;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;background:#000;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; min-height:780px; height:780px; background:var(--stage); border-radius:36px;
    overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.5); display:flex; flex-direction:column;
    position:relative;
  }

  /* ---------- STAGE (top two-thirds: camera is ALWAYS live, in every state) ---------- */
  .stage{
    flex:1; min-height:0; background:radial-gradient(120% 90% at 50% 0%, #101015 0%, #07070A 60%);
    position:relative; display:flex; flex-direction:column; overflow:hidden;
  }
  .stage-topbar{
    display:flex; align-items:center; justify-content:space-between; padding:16px 20px 0;
    font-family:'Manrope'; font-size:12px; font-weight:700; letter-spacing:.08em; color:var(--muted);
  }
  .stage-topbar .dw{ color:var(--accent); font-weight:800; }
  .live-dot{ display:inline-flex; align-items:center; gap:6px; }
  .live-dot span.dot{ width:7px; height:7px; border-radius:50%; background:var(--accent); box-shadow:0 0 8px var(--accent); animation:pulse 1.6s ease-in-out infinite; }
  @keyframes pulse{ 0%,100%{opacity:1;} 50%{opacity:.35;} }
  .stage-body{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:14px 26px; gap:14px; text-align:center; }
  .stage-hint{ font-family:'Spectral',serif; font-weight:600; font-size:23px; line-height:1.3; color:var(--ink); max-width:300px; }
  .stage-status{ font-family:'Manrope'; font-size:14px; font-weight:700; color:var(--muted); letter-spacing:.03em; }

  .viewfinder{ width:230px; height:230px; position:relative; display:flex; align-items:center; justify-content:center; }
  .corner{ position:absolute; width:34px; height:34px; border-color:var(--accent); }
  .corner.tl{ top:0; left:0; border-top:3px solid; border-left:3px solid; border-radius:10px 0 0 0; }
  .corner.tr{ top:0; right:0; border-top:3px solid; border-right:3px solid; border-radius:0 10px 0 0; }
  .corner.bl{ bottom:0; left:0; border-bottom:3px solid; border-left:3px solid; border-radius:0 0 0 10px; }
  .corner.br{ bottom:0; right:0; border-bottom:3px solid; border-right:3px solid; border-radius:0 0 10px 0; }
  .viewfinder .glyph{ font-size:44px; opacity:.9; }

  .idle-orb{
    width:150px; height:150px; border-radius:50%;
    background:radial-gradient(circle at 35% 30%, rgba(255,138,66,.35), rgba(255,138,66,0) 65%);
    border:2px solid var(--line); display:flex; align-items:center; justify-content:center; font-size:46px;
  }

  .swatch-card{
    width:190px; height:190px; border-radius:24px; box-shadow:0 14px 30px rgba(0,0,0,.5);
    display:flex; align-items:flex-end; justify-content:center; position:relative; overflow:hidden;
  }
  .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#5f7a5c 18px 36px);}
  .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#4c637f 16px 32px);}
  .sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#a9711f 14px 28px);}
  .item-caption{ margin-top:2px; }
  .item-name{ font-family:'Spectral',serif; font-weight:700; font-size:20px; margin:0; color:var(--ink); }
  .item-meta{ font-family:'Manrope'; font-size:13px; color:var(--muted); margin:4px 0 0; }

  .big-emoji{ font-size:60px; }

  /* toast, floats over the still-live stage */
  .toast{
    position:absolute; left:50%; bottom:14px; transform:translateX(-50%);
    background:rgba(20,14,10,.94); border:2px solid var(--accent); border-radius:999px;
    padding:10px 20px; font-weight:800; font-size:14px; display:flex; align-items:center; gap:8px;
    box-shadow:0 10px 24px rgba(0,0,0,.4); white-space:nowrap;
  }

  /* ---------- THUMB CONTROLLER (bottom third: the ONE persistent panel) ---------- */
  .panel{
    background:linear-gradient(180deg, var(--panel-edge), var(--panel));
    border-top:1px solid var(--line);
    border-radius:26px 26px 0 0;
    padding:16px 20px 24px;
    display:flex; flex-direction:column; gap:12px;
    min-height:298px;
    box-shadow:0 -18px 40px rgba(0,0,0,.35);
    position:relative;
  }
  .panel-head{ display:flex; align-items:center; gap:12px; }
  .home-btn{
    flex:0 0 auto; width:56px; height:56px; border-radius:16px; background:#241A13;
    border:2px solid var(--line); color:var(--muted); font-size:20px; display:flex;
    align-items:center; justify-content:center; cursor:pointer;
  }
  .home-btn:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
  .panel-title{ font-family:'Spectral',serif; font-weight:600; font-size:21px; line-height:1.25; color:var(--ink); flex:1; }

  .panel-body{ flex:1; display:flex; flex-direction:column; gap:12px; justify-content:center; }

  button{ font-family:'Manrope',sans-serif; cursor:pointer; }
  .pbtn{
    width:100%; min-height:60px; border-radius:16px; font-size:17px; font-weight:800;
    background:#241A13; border:2px solid var(--line); color:var(--ink); padding:10px 16px;
  }
  .pbtn-primary{ background:var(--accent); border-color:var(--accent); color:var(--accent-ink); }
  .pbtn-secondary{ background:transparent; border-color:var(--line); color:var(--muted); }
  .pbtn:focus-visible, .home-btn:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }

  .chip-row{ display:flex; gap:10px; overflow-x:auto; padding:4px 2px 8px; scroll-snap-type:x proximity; }
  .chip{
    flex:0 0 auto; scroll-snap-align:start; min-height:56px; padding:0 20px; border-radius:999px;
    background:#241A13; border:2px solid var(--line); color:var(--ink); font-size:15px; font-weight:700;
    display:flex; align-items:center; white-space:nowrap;
  }
  .chip:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }

  .shutter-wrap{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; flex:1; }
  .shutter{
    width:84px; height:84px; border-radius:50%; background:var(--accent); border:5px solid #4A2C16;
    font-size:30px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 6px rgba(255,138,66,.16);
  }
  .shutter-caption{ font-size:13px; font-weight:700; color:var(--muted); letter-spacing:.03em; }

  .linktext{
    background:none; border:none; color:var(--muted); font-size:15px; text-decoration:underline;
    text-align:center; min-height:56px; min-width:56px; width:100%;
  }
  .linktext:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }

  /* pull-up find drawer tab, lives on the LIVE screen */
  .drawer-tab{
    width:100%; min-height:56px; border-radius:16px; background:transparent;
    border:2px dashed var(--line); color:var(--muted); font-weight:800; font-size:15px;
    display:flex; align-items:center; justify-content:center; gap:8px;
  }
  .drawer-tab .chev{ color:var(--accent); }

  /* the vertical 3-option thumb picker (post-shot job wheel) */
  .picker-hint{ text-align:center; font-size:12px; font-weight:800; letter-spacing:.08em; color:var(--muted); text-transform:uppercase; }
  .picker-stack{ display:flex; flex-direction:column; gap:10px; touch-action:pan-y; }
  .picker-card{
    min-height:64px; width:100%; border-radius:16px; background:#241A13; border:2px solid var(--line);
    display:flex; align-items:center; gap:14px; padding:0 18px; font-weight:800; font-size:16px; color:var(--ink);
    text-align:left;
  }
  .picker-card .ic{ font-size:26px; }
  .picker-card.likely{ border-color:var(--accent); background:linear-gradient(135deg, rgba(255,138,66,.20), #241A13); }
  .picker-card.likely::after{ content:'MOST LIKELY'; margin-left:auto; font-size:10px; letter-spacing:.06em; color:var(--accent); font-weight:800; }
  .picker-card:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }

  /* hold-to-confirm ring, replaces every yes/no screen */
  .hold-wrap{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; flex:1; }
  .hold-ring{
    --pct:0%;
    width:104px; height:104px; border-radius:50%; padding:6px; border:none;
    background:conic-gradient(var(--accent) var(--pct), #2A1F17 var(--pct));
    display:flex; align-items:center; justify-content:center; transition:box-shadow .15s;
  }
  .hold-ring.holding{ box-shadow:0 0 0 8px rgba(255,138,66,.14); }
  .hold-ring-inner{
    width:100%; height:100%; border-radius:50%; background:#18120E; border:2px solid var(--line);
    display:flex; align-items:center; justify-content:center; text-align:center; padding:6px;
    font-size:13px; font-weight:800; color:var(--ink); line-height:1.2;
  }
  .hold-caption{ font-size:13px; font-weight:700; color:var(--muted); letter-spacing:.02em; text-align:center; }

  /* mini persistent shutter FAB — sits on the stage/panel seam in every non-idle state */
  .mini-fab{
    position:absolute; top:-28px; right:22px; width:64px; height:64px; border-radius:50%;
    background:var(--accent); border:4px solid #4A2C16; color:var(--accent-ink); font-size:24px;
    display:flex; align-items:center; justify-content:center; box-shadow:0 8px 18px rgba(0,0,0,.4); z-index:5;
  }
  .mini-fab:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
</style>
</head>
<body>
<div class="phone">
  <div class="stage" id="stage"></div>
  <div class="panel" id="panel"></div>
</div>

<script>
/* ---------------------------------------------------------------------
   NEW MODEL: "shoot first, one-thumb confirm"
   - Camera is ALWAYS live on the stage, in every state.
   - The bottom panel is a single persistent thumb controller.
   - Tap the shutter (big one on the idle screen, or the mini FAB seam
     button everywhere else) from ANY state to take a shot.
   - After a shot taken from idle, the panel morphs into a vertical
     3-option thumb PICKER (swipe up/down or tap) offering the most
     likely job first, instead of a menu screen.
   - Every yes/no decision is a HOLD-TO-CONFIRM ring (0.6s) instead of
     a Yes/No screen.
   - Find is a pull-up search drawer from the panel, not its own flow.
--------------------------------------------------------------------- */

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 JOB_META = {
  new:    {ic:'🆕', label:'Add New Item'},
  update: {ic:'🔄', label:'Update an Item'},
  sample: {ic:'📦', label:'Sample Came In'},
};

const state = {
  screen:'live',      // live | picker | new-brand | new-confirm |
                       // update-confirm | update-shoot | update-save |
                       // find-drawer | find-result | find-detail |
                       // sample-confirm | sample-printing
  job:null, item:null, brand:null, findIndex:0,
  pickerOrder:['new','update','sample'],
  toast:null, toastTimer:null,
};

function go(screen){ state.screen = screen; render(); }

function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }

/* the ONE shoot action — captures pretend data */
function shoot(cb){ state.item = randomItem(); cb && cb(); }

/* tapping the shutter from idle (or the persistent mini-FAB anywhere else)
   always leads to the picker: shoot first, decide the job second */
function mainShoot(){
  shoot(()=>{
    const likely = state.job || 'new';
    state.pickerOrder = ['new','update','sample'];
    state.pickerOrder.sort((a,b)=> (a===likely?-1:0) - (b===likely?-1:0));
    go('picker');
  });
}

function choosePicker(job){
  state.job = job;
  if(job==='new') go('new-brand');
  else if(job==='update') go('update-confirm');
  else if(job==='sample') go('sample-confirm');
}
function swipePicker(dir){
  const o = state.pickerOrder;
  if(dir>0) o.push(o.shift()); else o.unshift(o.pop());
  render();
}

function chooseBrand(b){ state.brand = b; go('new-confirm'); }
function completeNew(){ state.toast = {emoji:'🎉', text:'Saved!'}; state.job=null; go('live'); }

function confirmUpdateMatch(){ go('update-shoot'); }
function shootUpdate(){ shoot(()=> go('update-save')); }
function completeUpdateSave(){ state.toast = {emoji:'✅', text:'Updated!'}; state.job=null; go('live'); }

function openFindDrawer(){ go('find-drawer'); }
function startFind(){ state.findIndex = 0; go('find-result'); }
function nextFindResult(){ state.findIndex = (state.findIndex+1) % MOCK_ITEMS.length; render(); }
function confirmFindMatch(){ go('find-detail'); }
function updateFromFind(){ go('update-shoot'); }

function confirmSampleReceived(){
  go('sample-printing');
  setTimeout(()=>{ state.toast = {emoji:'✅', text:'Received!'}; state.job=null; go('live'); }, 1100);
}

/* ---------- stage fragments (camera is always "live" behind everything) ---------- */
function stageTopbar(){
  return `<div class="stage-topbar"><span class="dw">DW</span><span class="live-dot"><span class="dot"></span>LIVE</span></div>`;
}
function stageLive(){
  return `${stageTopbar()}
    <div class="stage-body">
      <div class="idle-orb">📷</div>
      <div class="stage-hint">Ready when you are.</div>
    </div>`;
}
function stageViewfinder(hint){
  return `${stageTopbar()}
    <div class="stage-body">
      <div class="stage-hint">${hint}</div>
      <div class="viewfinder">
        <div class="corner tl"></div><div class="corner tr"></div>
        <div class="corner bl"></div><div class="corner br"></div>
        <span class="glyph">🖼️</span>
      </div>
    </div>`;
}
function stageCaptured(caption){
  const it = state.item || MOCK_ITEMS[0];
  return `${stageTopbar()}
    <div class="stage-body">
      <div class="swatch-card ${it.sw}"></div>
      <div class="item-caption">
        <p class="item-name">${it.name} — ${it.color}</p>
        <p class="item-meta">${caption}</p>
      </div>
    </div>`;
}
function stageMessage(emoji, text, sub){
  return `${stageTopbar()}
    <div class="stage-body">
      <div class="big-emoji">${emoji}</div>
      <div class="stage-hint">${text}</div>
      ${sub ? `<div class="stage-status">${sub}</div>` : ''}
    </div>`;
}
function toastHTML(){
  return state.toast ? `<div class="toast">${state.toast.emoji} ${state.toast.text}</div>` : '';
}

/* ---------- panel fragments ---------- */
function homeBtn(){ return `<button class="home-btn" aria-label="Home" onclick="go('live')">⌂</button>`; }
function panelHead(title){
  return `<div class="panel-head">${state.screen!=='live' ? homeBtn() : ''}<div class="panel-title">${title}</div></div>`;
}
function miniFabHTML(){
  return `<button class="mini-fab" aria-label="Take a new photo" onclick="mainShoot()">📸</button>`;
}
function holdRingHTML(id, label, sub){
  return `<div class="hold-wrap">
    <button type="button" class="hold-ring" id="${id}"><span class="hold-ring-inner">${label}</span></button>
    <div class="hold-caption">${sub}</div>
  </div>`;
}
function pickerCardHTML(job, isLikely){
  const m = JOB_META[job];
  return `<button class="picker-card ${isLikely?'likely':''}" onclick="choosePicker('${job}')">
    <span class="ic">${m.ic}</span><span>${m.label}</span>
  </button>`;
}

/* ---------- hold-to-confirm mechanics ---------- */
function attachHold(btnId, duration, onComplete){
  const btn = document.getElementById(btnId);
  if(!btn) return;
  let raf=null, startTs=null, done=false;
  function frame(ts){
    if(startTs===null) startTs = ts;
    const pct = Math.min(1, (ts-startTs)/duration);
    btn.style.setProperty('--pct', (pct*100)+'%');
    if(pct>=1 && !done){ done=true; setTimeout(onComplete, 100); }
    else if(!done){ raf = requestAnimationFrame(frame); }
  }
  function start(e){ e.preventDefault(); if(done) return; startTs=null; btn.classList.add('holding'); raf=requestAnimationFrame(frame); }
  function cancel(){ if(done) return; if(raf) cancelAnimationFrame(raf); startTs=null; btn.classList.remove('holding'); btn.style.setProperty('--pct','0%'); }
  btn.addEventListener('pointerdown', start);
  btn.addEventListener('pointerup', cancel);
  btn.addEventListener('pointerleave', cancel);
  btn.addEventListener('pointercancel', cancel);
}
function wirePickerSwipe(){
  const el = document.querySelector('.picker-stack');
  if(!el) return;
  let startY = null;
  el.addEventListener('touchstart', e=>{ startY = e.touches[0].clientY; }, {passive:true});
  el.addEventListener('touchend', e=>{
    if(startY===null) return;
    const dy = e.changedTouches[0].clientY - startY;
    if(Math.abs(dy) > 24) swipePicker(dy<0 ? 1 : -1);
    startY = null;
  }, {passive:true});
}
function wireInteractive(){
  if(state.screen==='picker') wirePickerSwipe();
  if(state.screen==='new-confirm') attachHold('holdNew', 600, completeNew);
  if(state.screen==='update-confirm') attachHold('holdMatch', 600, confirmUpdateMatch);
  if(state.screen==='update-save') attachHold('holdUpdateSave', 600, completeUpdateSave);
  if(state.screen==='find-result') attachHold('holdFind', 600, confirmFindMatch);
  if(state.screen==='sample-confirm') attachHold('holdSample', 600, confirmSampleReceived);
}

/* ---------- render ---------- */
function render(){
  const stage = document.getElementById('stage');
  const panel = document.getElementById('panel');
  let s = '', p = '';

  if(state.screen==='live'){
    s = stageLive();
    p = `${panelHead('Point & shoot')}
      <div class="panel-body">
        <div class="shutter-wrap">
          <button class="shutter" aria-label="Take photo" onclick="mainShoot()">📸</button>
          <div class="shutter-caption">Tap the shutter — we'll ask what it's for</div>
        </div>
        <button class="drawer-tab" onclick="openFindDrawer()">🔎 Find an item <span class="chev">▲</span></button>
      </div>`;
  }

  else if(state.screen==='picker'){
    s = stageCaptured('What was that for?');
    p = `${panelHead("Got it — what's this for?")}
      <div class="panel-body">
        <div class="picker-hint">↕ swipe or tap to pick</div>
        <div class="picker-stack">${state.pickerOrder.map((j,i)=>pickerCardHTML(j, i===0)).join('')}</div>
      </div>`;
  }

  else if(state.screen==='new-brand'){
    s = stageCaptured('New item spotted');
    p = `${miniFabHTML()}${panelHead('Which brand is it?')}
      <div class="panel-body">
        <div class="chip-row">${BRANDS.map(b=>`<button class="chip" onclick="chooseBrand('${b}')">${b}</button>`).join('')}</div>
      </div>`;
  }
  else if(state.screen==='new-confirm'){
    s = stageCaptured(state.brand || 'Ready to save');
    p = `${miniFabHTML()}${panelHead('Save this item?')}
      <div class="panel-body">${holdRingHTML('holdNew', 'HOLD<br>TO SAVE', 'Press and hold 0.6s to confirm')}</div>`;
  }

  else if(state.screen==='update-confirm'){
    s = stageCaptured('Is this the item?');
    p = `${miniFabHTML()}${panelHead('Confirm the match')}
      <div class="panel-body">
        ${holdRingHTML('holdMatch', "HOLD —<br>YES", 'Hold if this is the right item')}
        <button class="linktext" onclick="mainShoot()">Not this — reshoot</button>
      </div>`;
  }
  else if(state.screen==='update-shoot'){
    s = stageViewfinder('Point at the replacement, then tap shutter.');
    p = `${panelHead('Shoot the new photo')}
      <div class="panel-body">
        <div class="shutter-wrap">
          <button class="shutter" aria-label="Take photo" onclick="shootUpdate()">📸</button>
          <div class="shutter-caption">Tap the shutter to capture</div>
        </div>
      </div>`;
  }
  else if(state.screen==='update-save'){
    s = stageCaptured('Ready to save the update');
    p = `${miniFabHTML()}${panelHead('Save the update?')}
      <div class="panel-body">${holdRingHTML('holdUpdateSave', 'HOLD<br>TO SAVE', 'Press and hold 0.6s to confirm')}</div>`;
  }

  else if(state.screen==='find-drawer'){
    s = stageLive();
    p = `${panelHead('Find an item')}
      <div class="panel-body">
        <button class="pbtn" onclick="startFind()">📷 Scan the Label</button>
        <button class="pbtn" onclick="startFind()">🎤 Say the Name</button>
        <button class="pbtn" onclick="startFind()">⌨️ Type It</button>
      </div>`;
  }
  else if(state.screen==='find-result'){
    state.item = MOCK_ITEMS[state.findIndex];
    s = stageCaptured('Is this the one?');
    p = `${miniFabHTML()}${panelHead('Confirm the item')}
      <div class="panel-body">
        ${holdRingHTML('holdFind', "HOLD —<br>IT'S THIS", 'Hold to confirm this match')}
        <button class="linktext" onclick="nextFindResult()">Show Next Result</button>
      </div>`;
  }
  else if(state.screen==='find-detail'){
    s = stageCaptured('Confirmed match');
    p = `${panelHead(state.item.name)}
      <div class="panel-body">
        <button class="pbtn pbtn-primary" onclick="updateFromFind()">Update This</button>
        <button class="pbtn pbtn-secondary" onclick="go('find-drawer')">Search Again</button>
      </div>`;
  }

  else if(state.screen==='sample-confirm'){
    s = stageCaptured('Mark this sample received?');
    p = `${miniFabHTML()}${panelHead('Sample received?')}
      <div class="panel-body">${holdRingHTML('holdSample', 'HOLD TO<br>CONFIRM', 'Hold to mark it received')}</div>`;
  }
  else if(state.screen==='sample-printing'){
    s = stageMessage('🖨️', 'Printing your sticker…', 'One sec…');
    p = `${panelHead('Printing')}<div class="panel-body"></div>`;
  }

  if(state.toast) s += toastHTML();

  stage.innerHTML = s;
  panel.innerHTML = p;
  wireInteractive();

  if(state.toast && !state.toastTimer){
    state.toastTimer = setTimeout(()=>{ state.toast=null; state.toastTimer=null; render(); }, 1800);
  }
}

render();
</script>
</body>
</html>