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