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