← back to Dw Photo Capture

public/kid-simple/v16-widget-stack.html

365 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>v16 — Widget Stack</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@400;500;700;800&display=swap" rel="stylesheet">
<style>
  :root{
    --bg:#F4EEE4; --card:#FFFFFF; --ink:#2B241D; --muted:#8A7F6F; --line:#E7DFD1;
    --rust:#BD5B34; --teal:#2F6259; --plum:#5B3A5E; --gold:#C79A46; --green:#3F8F5F;
  }
  *{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; min-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;
  }
  .screen{ flex:1; display:flex; flex-direction:column; padding:22px 20px 26px; overflow-y:auto; }
  button{ font-family:'Manrope',sans-serif; cursor:pointer; }
  h1,h2{ margin:0; }
  .question{
    font-family:'Cormorant Garamond',serif; font-weight:700; font-size:26px; line-height:1.18;
    text-align:center; margin:0 0 14px; color:var(--ink);
  }
  .rows{ display:flex; flex-direction:column; gap:12px; margin-top:14px; }
  .bigbtn{
    width:100%; min-height:56px; border-radius:18px; font-size:17px; font-weight:700;
    background:#fff; border:2px solid var(--line); color:var(--ink); padding:12px 18px;
  }
  .bigbtn-primary{ background:var(--accent,var(--gold)); border:2px solid var(--accent,var(--gold)); color:#fff; }
  .bigbtn-secondary{ background:#fff; border:2px solid var(--muted); color:var(--muted); }
  .bigbtn:focus-visible, .linktext:focus-visible, .widget:focus-visible, .navchip:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
  .linktext{
    background:none; border:none; color:var(--muted); font-size:15.5px; text-decoration:underline;
    text-align:center; min-height:56px; width:100%;
  }

  /* ---------- Home: widget stack (collapsed) ---------- */
  .topbar{ display:flex; align-items:baseline; justify-content:space-between; padding:6px 2px 18px; }
  .dw{ font-family:'Cormorant Garamond',serif; font-weight:700; font-size:20px; letter-spacing:.02em; }
  .today{ font-size:12px; color:var(--muted); font-weight:600; }
  .widgetstack{ display:flex; flex-direction:column; gap:14px; }
  .widget{
    background:var(--card); border-radius:26px; padding:18px 18px 16px; box-shadow:0 8px 22px rgba(43,36,29,.08);
    border:1px solid var(--line); text-align:left; transition:transform .12s ease; -webkit-tap-highlight-color:transparent;
  }
  .widget:active{ transform:scale(.98); }
  .widget-top{ display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
  .chip{
    width:44px; height:44px; border-radius:14px; background:var(--accent); color:#fff; font-size:21px;
    display:flex; align-items:center; justify-content:center; flex:none;
  }
  .widget-num{ font-family:'Cormorant Garamond',serif; font-weight:700; font-size:38px; color:var(--ink); }
  .widget-thumb{ width:44px; height:44px; border-radius:12px; flex:none; }
  .widget-label{ font-size:16px; font-weight:800; margin:2px 0 2px; }
  .widget-sub{ font-size:13.5px; color:var(--muted); margin-bottom:14px; }
  .widget-btn{
    width:100%; min-height:56px; border-radius:14px; border:none; background:var(--accent); color:#fff;
    font-size:15.5px; font-weight:800; letter-spacing:.01em;
  }

  .sw1{background:repeating-linear-gradient(45deg,#BD5B34 0 16px,#a04b29 16px 32px);}
  .sw2{background:repeating-linear-gradient(-45deg,#2F6259 0 14px,#254f47 14px 28px);}
  .sw3{background:repeating-linear-gradient(90deg,#5B3A5E 0 14px,#48304a 14px 28px);}
  .swatch{ width:100%; height:120px; border-radius:16px; margin-bottom:10px; }
  .swatch.small{ height:90px; }

  .itemname{ font-size:18px; font-weight:800; margin:0; text-align:center; }
  .itemmeta{ font-size:14.5px; color:var(--muted); margin:4px 0 0; text-align:center; }

  /* ---------- Expand-in-place: chip row for the OTHER widgets ---------- */
  .chiprow{ display:flex; gap:10px; margin-bottom:14px; }
  .navchip{
    flex:1; min-height:56px; min-width:56px; border-radius:16px; background:#fff;
    border:2px solid var(--line); border-top:4px solid var(--accent,var(--gold));
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
    font-size:11px; font-weight:800; color:var(--ink); padding:6px 4px;
  }
  .navchip .navicon{ font-size:19px; line-height:1; }

  /* ---------- The expanded widget card — job happens IN HERE ---------- */
  .expandedcard{
    background:var(--card); border-radius:26px; padding:18px 18px 20px; box-shadow:0 12px 30px rgba(43,36,29,.14);
    border:2px solid var(--accent,var(--gold)); flex:1; display:flex; flex-direction:column;
  }
  .expandedcard .widget-top{ margin-bottom:6px; }
  .dots{ display:flex; gap:6px; margin-left:auto; align-items:center; }
  .dot{ width:8px; height:8px; border-radius:50%; background:var(--line); }
  .dot.active{ background:var(--accent,var(--gold)); }
  .stepbody{ margin-top:8px; display:flex; flex-direction:column; flex:1; }
  .miniCenter{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:8px; text-align:center; }

  /* inline camera viewport, lives INSIDE the card (not full-bleed) */
  .camview{
    background:#1c1c1e; position:relative; display:flex; align-items:center; justify-content:center;
    color:#fff; min-height:200px; border-radius:20px; overflow:hidden; margin-bottom:4px;
  }
  .camview .hint{
    position:absolute; top:18px; left:14px; right:14px; text-align:center;
    font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:700; color:#fdfbf7;
  }
  .camview .status{ font-size:15px; font-weight:700; color:#f0e8d8; }
  .shutter{
    position:absolute; bottom:16px; left:50%; transform:translateX(-50%);
    width:72px; height:72px; border-radius:50%; background:var(--accent,var(--gold)); border:5px solid #fff;
    font-size:26px; display:flex; align-items:center; justify-content:center;
  }
  .altrow{ display:flex; gap:10px; margin-top:12px; }
  .altrow .linktext{ flex:1; border:2px solid var(--line); border-radius:14px; font-size:13.5px; text-decoration:none; font-weight:700; }
  .brandrows{ display:flex; flex-direction:column; gap:10px; max-height:280px; overflow-y:auto; padding-right:2px; }
</style>
</head>
<body>
<div class="phone">
  <div class="screen" id="screen" role="main"></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 ACCENT = { new:'#C79A46', update:'#2F6259', find:'#5B3A5E', sample:'#BD5B34', home:'#8A7F6F' };
const JOB_META = {
  sample:{ icon:'📦', short:'Samples', title:'Log a Sample' },
  update:{ icon:'🔄', short:'Updates', title:'Update an Item' },
  new:   { icon:'🆕', short:'New',     title:'Add New Item' },
  find:  { icon:'🔍', short:'Find',    title:'Find an Item' },
};
// Card-local step lists, per job — each job is a stack of IN-CARD steps, not full screens.
const STEPS = {
  sample:['cam','confirm','printing','done'],
  update:['cam','confirm','cam2','done'],
  new:   ['cam','brand','save','done'],
  find:  ['cam','result','detail'],
};

let counts = { sample:3, update:2 };
let lastAdded = { ...MOCK_ITEMS[0] };

// state.expanded = which widget card is currently open in place (or null = home stack).
// state.step = the CARD-LOCAL step within that widget. Nothing here is a full screen.
const state = { expanded:null, step:null, item:null, brand:null, resultIndex:0, exhausted:false };

function accentFor(job){ return ACCENT[job] || '#C79A46'; }
function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }

function openWidget(job){
  state.expanded = job; state.item = null; state.brand = null; state.resultIndex = 0; state.exhausted = false;
  state.step = 'cam'; // every job's first in-card step is always the camera
  render();
}
function switchWidget(job){ openWidget(job); }
function closeWidget(){ state.expanded = null; state.step = null; render(); }
function go(step){ state.step = step; render(); }

function shootInline(cb){
  const st = document.getElementById('camStatus');
  const btn = document.querySelector('.shutter');
  if(btn) btn.disabled = true;
  if(st) st.textContent = 'Snapping…';
  setTimeout(()=>{ state.item = state.item || randomItem(); cb(); }, 700);
}
function cameraCard(hint, onShoot){
  return `<div class="camview">
    <div class="hint">${hint}</div>
    <div class="status" id="camStatus">Tap the shutter</div>
    <button class="shutter" aria-label="Take photo" onclick="${onShoot}">📸</button>
  </div>`;
}
function dots(job){
  const steps = STEPS[job]; const idx = steps.indexOf(state.step);
  return `<div class="dots">${steps.map((s,i)=>`<span class="dot${i===idx?' active':''}"></span>`).join('')}</div>`;
}

/* ---------- job bodies: the job happens INSIDE the expanded card ---------- */
function sampleBody(){
  if(state.step==='cam') return cameraCard('Scan the box label.', "shootInline(()=>go('confirm'))");
  if(state.step==='confirm') return `
    <p class="itemname">${state.item.name} — ${state.item.color}</p>
    <div class="swatch small ${state.item.sw}"></div>
    <p class="itemmeta">code ${state.item.code}</p>
    <div class="rows"><button class="bigbtn bigbtn-primary" onclick="go('printing')">Mark Received</button></div>`;
  if(state.step==='printing') return `<div class="miniCenter"><p class="itemmeta">🖨️ Printing your sticker…</p></div>`;
  if(state.step==='done'){
    if(counts.sample>0) return `
      <div class="miniCenter"><p class="itemname">Logged! ✅</p><p class="itemmeta">Samples Waiting: ${counts.sample}</p></div>
      <div class="rows">
        <button class="bigbtn bigbtn-primary" onclick="openWidget('sample')">Next sample (${counts.sample} left) →</button>
        <button class="linktext" onclick="closeWidget()">Close</button>
      </div>`;
    return `
      <div class="miniCenter"><p class="itemname">All caught up! 🎉</p><p class="itemmeta">No samples waiting.</p></div>
      <div class="rows"><button class="bigbtn bigbtn-primary" onclick="closeWidget()">Close</button></div>`;
  }
}
function finishSample(){ counts.sample = Math.max(0, counts.sample-1); state.step='done'; render(); }

function updateBody(){
  if(state.step==='cam') return cameraCard('Scan the label or pattern.', "shootInline(()=>go('confirm'))");
  if(state.step==='confirm') return `
    <p class="itemname">${state.item.name} — ${state.item.color}</p>
    <div class="swatch small ${state.item.sw}"></div>
    <p class="itemmeta">code ${state.item.code}</p>
    <div class="rows">
      <button class="bigbtn bigbtn-primary" onclick="go('cam2')">Yes, That's It</button>
      <button class="bigbtn bigbtn-secondary" onclick="state.item=null; go('cam')">No, Scan Again</button>
    </div>`;
  if(state.step==='cam2') return cameraCard('Take the new photo.', "shootInline(()=>finishUpdate())");
  if(state.step==='done') return `
    <div class="miniCenter"><p class="itemname">Updated! ✅</p><p class="itemmeta">Need Updates: ${counts.update} left</p></div>
    <div class="rows"><button class="bigbtn bigbtn-primary" onclick="closeWidget()">Close</button></div>`;
}
function finishUpdate(){ counts.update = Math.max(0, counts.update-1); state.step='done'; render(); }

function newBody(){
  if(state.step==='cam') return cameraCard('Take a photo of it.', "shootInline(()=>go('brand'))");
  if(state.step==='brand') return `
    <p class="question" style="font-size:20px;">Which brand?</p>
    <div class="brandrows">${BRANDS.map(b=>`<button class="bigbtn" onclick="state.brand='${b}'; go('save')">${b}</button>`).join('')}</div>`;
  if(state.step==='save') return `
    <div class="swatch small ${state.item.sw}"></div>
    <p class="itemmeta">${state.brand} · code ${state.item.code}</p>
    <div class="rows">
      <button class="bigbtn bigbtn-primary" onclick="finishNew()">Save as Draft</button>
      <button class="linktext" onclick="go('cam')">Retake Photo</button>
    </div>`;
  if(state.step==='done') return `
    <div class="miniCenter"><p class="itemname">Saved! 🎉</p><p class="itemmeta">${lastAdded.name} · ${lastAdded.color}</p></div>
    <div class="rows">
      <button class="bigbtn bigbtn-primary" onclick="openWidget('new')">Add Another</button>
      <button class="linktext" onclick="closeWidget()">Close</button>
    </div>`;
}
function finishNew(){ lastAdded = { ...state.item, brand: state.brand }; state.step='done'; render(); }

function findBody(){
  if(state.step==='cam') return `
    ${cameraCard('Scan the label to find it.', "shootInline(()=>go('result'))")}
    <div class="altrow">
      <button class="linktext" onclick="state.item=randomItem(); go('result')">🎤 Say the Name</button>
      <button class="linktext" onclick="state.item=randomItem(); go('result')">⌨️ Type It</button>
    </div>`;
  if(state.step==='result'){
    if(state.exhausted) return `
      <div class="miniCenter"><p class="itemname">That's everything.</p><p class="itemmeta">Try another way?</p></div>
      <div class="altrow" style="margin-top:6px;">
        <button class="linktext" onclick="state.exhausted=false; state.resultIndex=0; go('cam')">📷 Scan</button>
        <button class="linktext" onclick="state.exhausted=false; state.resultIndex=0; state.item=randomItem(); go('result')">🎤 Say</button>
        <button class="linktext" onclick="state.exhausted=false; state.resultIndex=0; state.item=randomItem(); go('result')">⌨️ Type</button>
      </div>`;
    const it = MOCK_ITEMS[state.resultIndex];
    return `
      <p class="itemname">${it.name} — ${it.color}</p>
      <div class="swatch small ${it.sw}"></div>
      <p class="itemmeta">code ${it.code}</p>
      <div class="rows">
        <button class="bigbtn bigbtn-primary" onclick="pickResult()">Yes, This One</button>
        <button class="bigbtn bigbtn-secondary" onclick="nextResult()">Next Result</button>
      </div>`;
  }
  if(state.step==='detail') return `
    <div class="swatch small ${state.item.sw}"></div>
    <p class="itemname">${state.item.name}</p>
    <p class="itemmeta">${state.item.color} · code ${state.item.code}</p>
    <div class="rows">
      <button class="bigbtn bigbtn-primary" onclick="openWidgetForUpdateFromFind(state.item)">Update This</button>
      <button class="bigbtn bigbtn-secondary" onclick="state.resultIndex=0; state.exhausted=false; go('cam')">Search Again</button>
      <button class="linktext" onclick="closeWidget()">Close</button>
    </div>`;
}
function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('detail'); }
function nextResult(){
  if(state.resultIndex < MOCK_ITEMS.length-1){ state.resultIndex++; render(); }
  else { state.exhausted = true; render(); }
}
// Cross-job jump: Find already knows the item, so Update skips straight to its "new photo" step.
function openWidgetForUpdateFromFind(item){
  state.expanded='update'; state.item=item; state.brand=null; state.resultIndex=0; state.exhausted=false;
  state.step='cam2'; render();
}

function stepBody(){
  if(state.expanded==='sample') return sampleBody();
  if(state.expanded==='update') return updateBody();
  if(state.expanded==='new')    return newBody();
  if(state.expanded==='find')   return findBody();
  return '';
}

function chipRowHtml(){
  const jobs = ['sample','update','new','find'].filter(j=>j!==state.expanded);
  return `<div class="chiprow">
    <button class="navchip" style="--accent:${accentFor('home')}" onclick="closeWidget()"><span class="navicon">⌂</span><span>Home</span></button>
    ${jobs.map(j=>`<button class="navchip" style="--accent:${accentFor(j)}" onclick="switchWidget('${j}')"><span class="navicon">${JOB_META[j].icon}</span><span>${JOB_META[j].short}</span></button>`).join('')}
  </div>`;
}
function expandedCardHtml(){
  return `<div class="expandedcard" style="--accent:${accentFor(state.expanded)}">
    <div class="widget-top"><div class="chip">${JOB_META[state.expanded].icon}</div>${dots(state.expanded)}</div>
    <div class="widget-label">${JOB_META[state.expanded].title}</div>
    <div class="stepbody">${stepBody()}</div>
  </div>`;
}

function homeHtml(){
  return `
    <div class="topbar"><span class="dw">Designer Wallcoverings</span><span class="today">Today · Updated now</span></div>
    <div class="widgetstack">
      <div class="widget widget-lg" style="--accent:${ACCENT.sample}" tabindex="0" role="button" onclick="openWidget('sample')">
        <div class="widget-top"><div class="chip">📦</div><div class="widget-num">${counts.sample}</div></div>
        <div class="widget-label">Samples Waiting</div>
        <div class="widget-sub">Ready to scan &amp; log</div>
        <button class="widget-btn" onclick="event.stopPropagation(); openWidget('sample')">Log a Sample →</button>
      </div>

      <div class="widget widget-md" style="--accent:${ACCENT.update}" tabindex="0" role="button" onclick="openWidget('update')">
        <div class="widget-top"><div class="chip">🔄</div><div class="widget-num">${counts.update}</div></div>
        <div class="widget-label">Need Updates</div>
        <div class="widget-sub">New photos needed</div>
        <button class="widget-btn" onclick="event.stopPropagation(); openWidget('update')">Update an Item →</button>
      </div>

      <div class="widget widget-md" style="--accent:${ACCENT.new}" tabindex="0" role="button" onclick="openWidget('new')">
        <div class="widget-top"><div class="chip">🆕</div><div class="widget-thumb ${lastAdded.sw}"></div></div>
        <div class="widget-label">Last Added</div>
        <div class="widget-sub">${lastAdded.name} · ${lastAdded.color}</div>
        <button class="widget-btn" onclick="event.stopPropagation(); openWidget('new')">Add New Item →</button>
      </div>

      <div class="widget widget-sm" style="--accent:${ACCENT.find}" tabindex="0" role="button" onclick="openWidget('find')">
        <div class="widget-top"><div class="chip">🔍</div></div>
        <div class="widget-label">Find an Item</div>
        <div class="widget-sub">Search everything you've added</div>
        <button class="widget-btn" onclick="event.stopPropagation(); openWidget('find')">Find an Item →</button>
      </div>
    </div>`;
}

function render(){
  const s = document.getElementById('screen');
  s.style.setProperty('--accent', accentFor(state.expanded || 'home'));
  let html = '';
  if(!state.expanded){
    html = homeHtml();
  } else {
    html = chipRowHtml() + expandedCardHtml();
  }
  s.innerHTML = html;
  if(state.expanded==='sample' && state.step==='printing'){
    setTimeout(()=>{ if(state.expanded==='sample' && state.step==='printing'){ finishSample(); } }, 900);
  }
}

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