← back to Dw Photo Capture

public/kid-simple/v12-hold-to-talk.html

412 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>v12 — Hold-to-Talk</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=Cormorant+Garamond:wght@500;600;700&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
<style>
  :root{
    --bg:#F7EFE4; --ink:#2A2118; --panel:#FFFFFF; --line:#E7DCC9;
    --accent:#B5502E; --accent-dark:#8C3A1F; --teal:#2F5D57; --teal-dark:#20403C;
    --amber:#CE8B3C; --success:#4F8F6D; --cam:#181F1D; --muted:#8A7F6E;
  }
  *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
  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(--bg); border-radius:36px; overflow:hidden;
    box-shadow:0 24px 60px rgba(0,0,0,.28); display:flex; flex-direction:column; position:relative;
    user-select:none;
  }
  .screen{ flex:1; display:flex; flex-direction:column; overflow-y:auto; position:relative; }
  h1,h2{ font-family:'Cormorant Garamond', serif; margin:0; }
  button{ font-family:'Manrope',sans-serif; cursor:pointer; }

  /* ---- Home ---- */
  .homeWrap{ flex:1; display:flex; flex-direction:column; background:var(--cam); color:#F7EFE4; position:relative; }
  .homeWrap .grain{
    position:absolute; inset:0;
    background:
      radial-gradient(circle at 20% 15%, rgba(47,93,87,.55), transparent 55%),
      radial-gradient(circle at 85% 80%, rgba(181,80,46,.35), transparent 55%);
  }
  .dwmark{ position:relative; z-index:2; padding:22px 24px 0; font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:#CBB79A; font-weight:700; }
  .captionWrap{ position:relative; z-index:2; padding:18px 22px 0; flex:0 0 auto; }
  .captionPill{
    background:rgba(255,255,255,.06); border:1px solid rgba(247,239,228,.18); border-radius:20px;
    padding:16px 18px; min-height:64px; display:flex; align-items:center; gap:12px; backdrop-filter:blur(2px);
  }
  .captionDot{ width:10px; height:10px; border-radius:50%; background:var(--muted); flex:0 0 auto; transition:background .2s; }
  .captionDot.live{ background:var(--accent); box-shadow:0 0 0 0 rgba(181,80,46,.6); animation:pulseDot 1s infinite; }
  @keyframes pulseDot{ 0%{box-shadow:0 0 0 0 rgba(181,80,46,.55);} 70%{box-shadow:0 0 0 9px rgba(181,80,46,0);} 100%{box-shadow:0 0 0 0 rgba(181,80,46,0);} }
  .captionText{ font-size:17px; font-weight:600; line-height:1.35; color:#F7EFE4; }
  .captionText.idle{ color:#CBB79A; font-weight:500; font-size:16px; }

  .holdArea{ position:relative; z-index:2; flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:18px; padding:10px 24px; }
  .holdHint{ font-family:'Cormorant Garamond',serif; font-size:26px; font-weight:600; text-align:center; color:#F7EFE4; margin:0 0 4px; }
  .holdSub{ font-size:14px; color:#CBB79A; text-align:center; margin-top:-10px; }
  .micBtnWrap{ position:relative; width:168px; height:168px; display:flex; align-items:center; justify-content:center; }
  .ring{ position:absolute; inset:0; border-radius:50%; border:2px solid rgba(181,80,46,.0); transition:all .25s; }
  .micBtnWrap.listening .ring{ border-color:rgba(181,80,46,.55); animation:ringGrow 1.1s ease-out infinite; }
  @keyframes ringGrow{ 0%{ transform:scale(.82); opacity:.9;} 100%{ transform:scale(1.28); opacity:0;} }
  .micBtn{
    width:132px; height:132px; border-radius:50%; background:var(--accent); border:6px solid #F7EFE4;
    display:flex; align-items:center; justify-content:center; font-size:52px; color:#fff;
    box-shadow:0 10px 26px rgba(0,0,0,.4); transition:transform .12s, background .2s;
  }
  .micBtnWrap.listening .micBtn{ background:var(--accent-dark); transform:scale(1.06); }
  .micBtn:focus-visible{ outline:3px solid #F2C879; outline-offset:4px; }

  .fallbackWrap{ position:relative; z-index:2; padding:10px 20px 22px; }
  .fallbackLabel{ text-align:center; font-size:13px; color:#8F8272; margin:0 0 10px; letter-spacing:.04em; }
  .chipGrid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
  .chip{
    min-height:58px; border-radius:16px; background:rgba(255,255,255,.07); border:1.5px solid rgba(247,239,228,.22);
    color:#F7EFE4; font-size:14.5px; font-weight:700; padding:8px 10px; text-align:center; line-height:1.25;
  }
  .chip:active{ background:rgba(255,255,255,.16); }

  /* ---- generic content screens ---- */
  .pad{ flex:1; display:flex; flex-direction:column; padding:30px 24px 26px; background:var(--bg); }
  .kicker{ font-size:12.5px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--teal); margin:0 0 8px; }
  .question{ font-family:'Cormorant Garamond',serif; font-weight:600; font-size:30px; line-height:1.2; margin:0 0 22px; color:var(--ink); }

  .bigbtn{
    width:100%; min-height:60px; border-radius:16px; font-size:18px; font-weight:700;
    background:var(--panel); border:2px solid var(--line); color:var(--ink); padding:12px 18px; margin-bottom:12px;
  }
  .bigbtn-primary{ background:var(--accent); border:2px solid var(--accent); color:#fff; }
  .bigbtn-secondary{ background:#fff; border:2px solid var(--muted); color:var(--muted); }
  .bigbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
  .linktext{
    background:none; border:none; color:var(--muted); font-size:16px; text-decoration:underline;
    text-align:center; min-height:48px; width:100%;
  }

  .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
  .brandcard{
    width:100%; min-height:64px; border-radius:16px; background:#fff; border:2px solid var(--line);
    display:flex; align-items:center; gap:14px; padding:10px 14px; font-size:18px; font-weight:700; color:var(--ink);
  }
  .swatch{ width:44px; height:44px; border-radius:12px; flex:0 0 auto; }
  .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 9px,#6f9169 9px 18px);}
  .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 8px,#5d7897 8px 16px);}
  .sw3{background:repeating-linear-gradient(90deg,#CE8B3C 0 7px,#b0722a 7px 14px);}
  .sw4{background:repeating-linear-gradient(135deg,#B5502E 0 8px,#8c3a1f 8px 16px);}

  .itemcard{ background:#fff; border:2px solid var(--line); border-radius:20px; padding:20px; display:flex; flex-direction:column; align-items:center; gap:10px; margin-bottom:20px; }
  .itemcard .swatch.big{ width:100%; height:150px; border-radius:16px; }
  .itemname{ font-size:21px; font-weight:800; margin:2px 0 0; text-align:center; }
  .itemmeta{ font-size:15px; color:var(--muted); margin:0; text-align:center; }

  /* camera screens */
  .camwrap{ flex:1; display:flex; flex-direction:column; position:relative; background:var(--cam); }
  .camview{ flex:1; position:relative; display:flex; align-items:center; justify-content:center; color:#fff; }
  .camview .hint{
    position:absolute; top:44px; left:20px; right:20px; text-align:center;
    font-family:'Cormorant Garamond',serif; font-size:27px; font-weight:600; color:#F7EFE4;
  }
  .camview .flashmsg{ font-size:16px; font-weight:600; color:#CBB79A; }
  .shutter{
    position:absolute; bottom:36px; left:50%; transform:translateX(-50%);
    width:88px; height:88px; border-radius:50%; background:var(--accent); border:6px solid #fff;
    font-size:32px; display:flex; align-items:center; justify-content:center; color:#fff;
  }
  .shutter:focus-visible{ outline:3px solid #F2C879; outline-offset:4px; }

  .doneWrap{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px; padding:30px; background:var(--bg); text-align:center; }
  .doneBadge{ width:96px; height:96px; border-radius:50%; background:var(--success); display:flex; align-items:center; justify-content:center; font-size:44px; color:#fff; }
  .doneTitle{ font-family:'Cormorant Garamond',serif; font-size:30px; font-weight:700; margin:0; }

  .homefab{
    position:absolute; top:16px; left:16px; z-index:30; min-width:56px; min-height:56px;
    border-radius:16px; background:rgba(255,255,255,.94); border:2px solid var(--line); color:var(--ink);
    font-size:14px; font-weight:700; display:none; align-items:center; justify-content:center; gap:4px;
    padding:0 12px; box-shadow:0 4px 10px rgba(0,0,0,.16);
  }
  .homefab.onDark{ background:rgba(0,0,0,.4); border-color:rgba(255,255,255,.3); color:#fff; }
  .homefab:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
  .btnrow{ display:flex; gap:10px; width:100%; }
  .btnrow .bigbtn{ flex:1; }
</style>
</head>
<body>
<div class="phone">
  <button class="homefab" id="homeFab" aria-label="Home" onclick="go('home')">⌂ Home</button>
  <div class="screen" id="screen" role="main"></div>
</div>

<script>
const BRANDS = [
  {name:'Thibaut', sw:'sw1'},
  {name:'Kravet', sw:'sw2'},
  {name:'Cole & Son', sw:'sw3'},
  {name:'Groundworks', sw:'sw4'},
];
const MATCH_ITEM = {name:'Palm Damask', color:'Sage', code:'item 10423', sw:'sw1'};
const RESULTS = [
  {name:'Palm Damask', color:'Sage', code:'item 10423', sw:'sw1'},
  {name:'Trellis Weave', color:'Indigo', code:'item 88214', sw:'sw2'},
  {name:'Linen Bloom', color:'Blush', code:'item 55071', sw:'sw3'},
];

const PHRASES = [
  { steps:['Mm...', 'I have a...', 'I have a new...', '"Add a new item"'], go:'addCam' },
  { steps:['Hmm...', 'I need to...', 'I need to update...', '"Update an item"'], go:'updScan' },
  { steps:['Umm...', 'Where is...', 'Where is that...', '"Find an item"'], go:'findMethod' },
  { steps:['So...', 'A box just...', 'A box just came...', '"A sample came in"'], go:'sampScan' },
];
let phraseIdx = 0;
let holding = false, capStep = 0, capTimer = null, finishTimer = null;

function esc(s){ return s; }

const screen = document.getElementById('screen');
const homeFab = document.getElementById('homeFab');

function go(name, data){
  clearHold();
  homeFab.style.display = (name === 'home') ? 'none' : 'flex';
  const dark = ['addCam','updScan','updCam2','sampScan'].includes(name);
  homeFab.classList.toggle('onDark', dark);
  screen.innerHTML = RENDER[name](data || {});
  screen.scrollTop = 0;
}

function clearHold(){
  holding = false; capStep = 0;
  if(capTimer){ clearInterval(capTimer); capTimer = null; }
  if(finishTimer){ clearTimeout(finishTimer); finishTimer = null; }
}

function startHold(){
  if(holding) return;
  holding = true; capStep = 0;
  const wrap = document.getElementById('micWrap');
  const dot = document.getElementById('capDot');
  const txt = document.getElementById('capText');
  if(wrap) wrap.classList.add('listening');
  if(dot) dot.classList.add('live');
  const phrase = PHRASES[phraseIdx];
  txt.textContent = phrase.steps[0];
  txt.classList.remove('idle');
  capTimer = setInterval(()=>{
    capStep++;
    if(capStep >= phrase.steps.length){
      clearInterval(capTimer); capTimer = null;
      finishTimer = setTimeout(()=>{
        const nextRoute = phrase.go;
        phraseIdx = (phraseIdx + 1) % PHRASES.length;
        clearHold();
        go(nextRoute);
      }, 480);
      return;
    }
    txt.textContent = phrase.steps[capStep];
  }, 380);
}

function endHold(){
  if(!holding) return;
  const finishedFully = capTimer === null && finishTimer !== null;
  if(finishedFully){ return; } // let the auto-navigate finish
  clearHold();
  const wrap = document.getElementById('micWrap');
  const dot = document.getElementById('capDot');
  const txt = document.getElementById('capText');
  if(wrap) wrap.classList.remove('listening');
  if(dot) dot.classList.remove('live');
  if(txt){
    txt.textContent = "Didn't quite catch that — try again, or tap a job below";
    txt.classList.remove('idle');
    setTimeout(()=>{
      if(document.getElementById('capText') === txt){
        txt.textContent = 'Hold the button & tell me what you need';
        txt.classList.add('idle');
      }
    }, 1700);
  }
}

const RENDER = {
  home: () => `
    <div class="homeWrap">
      <div class="grain"></div>
      <div class="dwmark">Designer Wallcoverings</div>
      <div class="captionWrap">
        <div class="captionPill">
          <div class="captionDot" id="capDot"></div>
          <div class="captionText idle" id="capText">Hold the button & tell me what you need</div>
        </div>
      </div>
      <div class="holdArea">
        <p class="holdHint">Press &amp; hold, then say it</p>
        <div class="micBtnWrap" id="micWrap">
          <div class="ring"></div>
          <button class="micBtn" id="micBtn" aria-label="Hold and talk"
            onpointerdown="startHold()" onpointerup="endHold()" onpointerleave="endHold()" onpointercancel="endHold()">🎙️</button>
        </div>
        <p class="holdSub">"new item" · "update" · "find" · "sample came in"</p>
      </div>
      <div class="fallbackWrap">
        <p class="fallbackLabel">OR TAP INSTEAD</p>
        <div class="chipGrid">
          <button class="chip" onclick="go('addCam')">Add a New Item</button>
          <button class="chip" onclick="go('updScan')">Update an Item</button>
          <button class="chip" onclick="go('findMethod')">Find an Item</button>
          <button class="chip" onclick="go('sampScan')">A Sample Came In</button>
        </div>
      </div>
    </div>
  `,

  addCam: () => `
    <div class="camwrap">
      <div class="camview">
        <div class="hint">Take a photo of it.</div>
        <div class="flashmsg">Point the camera at the sample</div>
        <button class="shutter" aria-label="Shoot photo" onclick="go('addBrand')">📸</button>
      </div>
    </div>
  `,
  addBrand: () => `
    <div class="pad">
      <p class="kicker">Add a new item</p>
      <h2 class="question">Which brand is it?</h2>
      <div class="brandlist">
        ${BRANDS.map(b => `<button class="brandcard" onclick="go('addSave')"><span class="swatch ${b.sw}"></span>${b.name}</button>`).join('')}
      </div>
    </div>
  `,
  addSave: () => `
    <div class="pad">
      <p class="kicker">Add a new item</p>
      <h2 class="question">Save it?</h2>
      <div class="itemcard">
        <div class="swatch big sw1"></div>
        <p class="itemname">New Sample</p>
        <p class="itemmeta">Thibaut · Draft</p>
      </div>
      <button class="bigbtn bigbtn-primary" onclick="go('addDone')">Save as Draft</button>
      <button class="linktext" onclick="go('addCam')">Retake Photo</button>
    </div>
  `,
  addDone: () => `
    <div class="doneWrap">
      <div class="doneBadge">🎉</div>
      <h2 class="doneTitle">Saved!</h2>
      <div class="btnrow">
        <button class="bigbtn bigbtn-secondary" onclick="go('addCam')">Add Another</button>
        <button class="bigbtn bigbtn-primary" onclick="go('home')">Home</button>
      </div>
    </div>
  `,

  updScan: () => `
    <div class="camwrap">
      <div class="camview">
        <div class="hint">Show me the label or pattern.</div>
        <div class="flashmsg">Scan it in the frame</div>
        <button class="shutter" aria-label="Shoot photo" onclick="go('updMatch')">📸</button>
      </div>
    </div>
  `,
  updMatch: () => `
    <div class="pad">
      <p class="kicker">Update an item</p>
      <h2 class="question">Found it — is this the one?</h2>
      <div class="itemcard">
        <div class="swatch big ${MATCH_ITEM.sw}"></div>
        <p class="itemname">${MATCH_ITEM.name}</p>
        <p class="itemmeta">${MATCH_ITEM.color} · ${MATCH_ITEM.code}</p>
      </div>
      <button class="bigbtn bigbtn-primary" onclick="go('updCam2')">Yes</button>
      <button class="bigbtn bigbtn-secondary" onclick="go('findMethod')">No, Let Me Search</button>
    </div>
  `,
  updCam2: () => `
    <div class="camwrap">
      <div class="camview">
        <div class="hint">Take the new photo.</div>
        <div class="flashmsg">This replaces the old one</div>
        <button class="shutter" aria-label="Shoot photo" onclick="go('updDone')">📸</button>
      </div>
    </div>
  `,
  updDone: () => `
    <div class="doneWrap">
      <div class="doneBadge">✅</div>
      <h2 class="doneTitle">Updated!</h2>
      <button class="bigbtn bigbtn-primary" onclick="go('home')">Home</button>
    </div>
  `,

  findMethod: () => `
    <div class="pad">
      <p class="kicker">Find an item</p>
      <h2 class="question">How do you want to find it?</h2>
      <button class="bigbtn" onclick="go('findResults')">📷 Scan the Label</button>
      <button class="bigbtn" onclick="go('findResults')">🎤 Say the Name</button>
      <button class="bigbtn" onclick="go('findResults')">⌨️ Type It</button>
    </div>
  `,
  findResults: () => `
    <div class="pad">
      <p class="kicker">Find an item</p>
      <h2 class="question">Which one is it?</h2>
      <div class="brandlist">
        ${RESULTS.map(r => `<button class="brandcard" onclick="go('findDetail')"><span class="swatch ${r.sw}"></span>${r.name}</button>`).join('')}
      </div>
    </div>
  `,
  findDetail: () => `
    <div class="pad">
      <p class="kicker">Find an item</p>
      <div class="itemcard">
        <div class="swatch big ${MATCH_ITEM.sw}"></div>
        <p class="itemname">${MATCH_ITEM.name}</p>
        <p class="itemmeta">${MATCH_ITEM.color} · ${MATCH_ITEM.code}</p>
      </div>
      <button class="bigbtn bigbtn-primary" onclick="go('updCam2')">Update This</button>
      <button class="bigbtn bigbtn-secondary" onclick="go('findMethod')">Search Again</button>
    </div>
  `,

  sampScan: () => `
    <div class="camwrap">
      <div class="camview">
        <div class="hint">Scan the box label.</div>
        <div class="flashmsg">Line it up in the frame</div>
        <button class="shutter" aria-label="Shoot photo" onclick="go('sampMatch')">📸</button>
      </div>
    </div>
  `,
  sampMatch: () => `
    <div class="pad">
      <p class="kicker">A sample came in</p>
      <h2 class="question">Mark it received?</h2>
      <div class="itemcard">
        <div class="swatch big ${MATCH_ITEM.sw}"></div>
        <p class="itemname">${MATCH_ITEM.name}</p>
        <p class="itemmeta">${MATCH_ITEM.color} · ${MATCH_ITEM.code}</p>
      </div>
      <button class="bigbtn bigbtn-primary" onclick="go('sampDone')">Yes, Mark Received</button>
    </div>
  `,
  sampDone: () => `
    <div class="doneWrap">
      <div class="doneBadge">✅</div>
      <h2 class="doneTitle">Printing your sticker...<br>Done!</h2>
      <button class="bigbtn bigbtn-primary" onclick="go('home')">Home</button>
    </div>
  `,
};

go('home');
</script>
</body>
</html>