diff --git a/app/static/index.html b/app/static/index.html index 48f3eb0..64149ac 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -33,7 +33,8 @@ header{height:calc(50px + env(safe-area-inset-top,0px));display:flex;align-items .card{position:absolute;left:14px;right:14px;top:6px;bottom:34px;border-radius:18px;overflow:hidden;background:var(--card); box-shadow:0 0 0 1px rgba(255,255,255,.07), 0 18px 40px rgba(0,0,0,.5);will-change:transform;touch-action:none} .nophoto{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:repeating-linear-gradient(45deg,#141419 0 14px,#18181f 14px 28px);color:#55555f;font-size:13px;text-align:center;padding:24px;line-height:1.6} -.card .photo{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} +.card .photo{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 6px 24px rgba(0,0,0,.45))} +.phblur{position:absolute;inset:-18px;background-size:cover;background-position:center;background-repeat:no-repeat;filter:blur(22px) brightness(.55) saturate(1.1);transform:scale(1.08);pointer-events:none} .card .grad{position:absolute;left:0;right:0;bottom:0;height:52%;background:linear-gradient(180deg,rgba(0,0,0,0) 0%,rgba(0,0,0,.55) 55%,rgba(0,0,0,.88) 100%)} .card .info{position:absolute;left:16px;right:16px;bottom:14px;z-index:3} .chips{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap} @@ -66,8 +67,9 @@ nav{height:calc(68px + env(safe-area-inset-bottom,0px));display:flex;align-items .dgrab{flex:none;height:26px;display:flex;align-items:center;justify-content:center;background:#0f0f13;cursor:grab;touch-action:none} .dgrab i{width:42px;height:5px;border-radius:3px;background:rgba(255,255,255,.3);display:block} .dclose{position:absolute;top:38px;left:14px;width:36px;height:36px;border-radius:50%;background:rgba(0,0,0,.55);color:#fff;border:0;font-size:17px;z-index:5;cursor:pointer} -.dhero{position:relative;height:34%;min-height:190px;flex:none;background:#17171c;touch-action:none} -.dphoto{width:100%;height:100%;object-fit:cover;display:block} +.dhero{position:relative;height:34%;min-height:190px;flex:none;background:#17171c;touch-action:none;overflow:hidden} +.dblur{position:absolute;inset:-18px;background-size:cover;background-position:center;background-repeat:no-repeat;filter:blur(22px) brightness(.55) saturate(1.1);transform:scale(1.08);pointer-events:none} +.dphoto{width:100%;height:100%;object-fit:contain;display:block;filter:drop-shadow(0 6px 24px rgba(0,0,0,.45))} .dphoto.ph{opacity:.25} .dherograd{position:absolute;left:0;right:0;bottom:0;height:70px;background:linear-gradient(transparent,rgba(15,15,19,.95))} .dbody{flex:1;overflow-y:auto;padding:14px 18px 20px} @@ -242,7 +244,7 @@ nav{height:calc(68px + env(safe-area-inset-bottom,0px));display:flex;align-items
-
+
@@ -509,8 +511,10 @@ function segHTML(n,cur){ if(!n||n<2) return ''; return `
${segBars(n,cur)}
`; } +function escAttr(s){ return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,'''); } function phwrapHTML(src){ - return `
${src?``:`
Photo pending — thumbnail backfill queued
`}
`; + const img=src?`
`:`
Photo pending — thumbnail backfill queued
`; + return `
${img}
`; } function attachPhError(entry){ const wrap=entry.el.querySelector('.phwrap'); @@ -751,27 +755,34 @@ function upH(e){ else if(dy<-140) commit(el,'maybe'); else { el.style.transform=''; setStamps(el,0,0,0); if(!moved && e.type==='pointerup' && isTop(el)) tapZone(pile[0],e); } } -/* §11.2/§11.3/§11.4 tap zones: sides flip (clamped), center = fullscreen. - The old tap-anywhere -> dossier gesture is retired (ⓘ stays the door). */ +/* §11.4 v1.1 tap zones: sides flip (clamped), CENTER = dossier (bike data + first — the old center-tap→fullscreen and tap-anywhere→dossier gestures + are retired). The deck dossier remembers its pile entry so a viewer + opened from its hero walks back to it on Esc. */ function tapZone(entry,e){ - const n=nPhotos(entry.d); - if(n<1 || !entry.d.img) return; const r=stage.getBoundingClientRect(); const rel=e.clientX-r.left, w=r.width||360; - if(rel>=w/3 && rel<=w*2/3) openViewer(entry.d, entry.cur||0, {type:'card',entry}); - else flipCard(entry, rel=w/3 && rel<=w*2/3){ + dEntry=entry; // live pile entry (cur photo index) + openDetail(entry.d,{type:'deck'}); + }else{ + flipCard(entry, rel viewer) +let dEntry=null; // v1.1: pile entry when opened from the deck function openDetail(data,ctx){ dCtx=ctx||null; dData=data; + dEntry=null; // only tapZone re-sets it (deck path) const ph=$('dphoto'); - if(data.img){ ph.src=data.img; ph.classList.remove('ph'); } - else { ph.removeAttribute('src'); ph.classList.add('ph'); } - ph.onerror=()=>{ ph.removeAttribute('src'); ph.classList.add('ph'); }; + const blur=$('dblur'); + if(data.img){ ph.src=data.img; ph.classList.remove('ph'); blur.style.backgroundImage='url("'+data.img+'")'; } + else { ph.removeAttribute('src'); ph.classList.add('ph'); blur.style.backgroundImage='none'; } + ph.onerror=()=>{ ph.removeAttribute('src'); ph.classList.add('ph'); blur.style.backgroundImage='none'; }; $('dtitle').textContent=data.title||''; $('dprice').textContent=priceFor(data); $('dsub').textContent=subFor(data); @@ -819,7 +830,7 @@ function fillRank(data){ }); } } -function closeDetail(){ const d=$('detail'); d.style.transition=''; d.style.transform=''; d.classList.remove('on'); $('dback').classList.remove('on'); dCtx=null; } +function closeDetail(){ const d=$('detail'); d.style.transition=''; d.style.transform=''; d.classList.remove('on'); $('dback').classList.remove('on'); dCtx=null; dEntry=null; } $('dclose').onclick=closeDetail; $('dclose').addEventListener('pointerdown',e=>e.stopPropagation()); /* pull-down-to-close (grabber bar + photo) */ @@ -829,10 +840,12 @@ function dMoveH(e){ if(dDragY===null) return; const dy=Math.max(0,e.clientY-dDra function dEndH(e){ if(dDragY===null) return; const dy=Math.max(0,e.clientY-dDragY); dDragY=null; if(dy>110){ closeDetail(); } else { const d=$('detail'); d.style.transition='transform .25s cubic-bezier(.2,.8,.3,1)'; d.style.transform=''; - /* B1 §11.3: a tap on the dossier HERO opens the fullscreen viewer at photo 0 */ + /* B1 §11.3: a tap on the dossier HERO opens the fullscreen viewer at + photo 0. When the dossier sits over the deck, the viewer back-chain + walks Esc back to the dossier (item 2), then to the deck. */ if(e.type==='pointerup' && dy<=6 && e.currentTarget&&e.currentTarget.classList && e.currentTarget.classList.contains('dhero') && dData&&dData.img){ - openViewer(dData,0,{type:'dossier'}); + openViewer(dData,0,{type:'dossier', backToDetail:dEntry}); } } } [document.querySelector('.dgrab'),document.querySelector('.dhero')].forEach(el=>{ if(!el) return; el.addEventListener('pointerdown',dStart); el.addEventListener('pointermove',dMoveH); el.addEventListener('pointerup',dEndH); el.addEventListener('pointercancel',dEndH); }); function dAction(a){ @@ -881,6 +894,19 @@ function closeViewer(){ v.style.transition=''; v.style.transform=''; vCtx=null; vPtrs.clear(); vPinch=null; vDrag=null; } +/* v1.1 walk-back chain: when the viewer was reached from the deck dossier + hero, Esc returns to the dossier (photo 0) instead of the deck. */ +function vWalkBack(){ + const back=vCtx&&vCtx.back; + if(back&&back.backToDetail){ + const item=vCtx.item; + closeViewer(); + openDetail(item, back.backToDetail); + dEntry=back.backToDetail.entry||null; // keep the chain repeatable + } else { + closeViewer(); + } +} $('vback').addEventListener('click',closeViewer); $('vclose').onclick=closeViewer; $('vclose').addEventListener('pointerdown',e=>e.stopPropagation()); @@ -1260,13 +1286,28 @@ document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{ else if(name==='trends') fetchTrends(); }); -/* ---------- desktop keyboard arrows (DESIGN.md: v1 frontend) ---------- */ +/* ---------- v1.1 keyboard map (BACKLOG "Shortcuts"): ← nope · → like · + ↑ maybe · ↓ undo · Backspace = undo alias (kept for muscle memory) · + Esc closes the top open sheet (viewer → dossier → deck). Arrows act only + on the deck; text-input focus and open sheets swallow them; arrow-key + defaults are prevented so no page scroll can fire. -------------------- */ document.addEventListener('keydown',e=>{ - if(e.target.tagName==='INPUT'||$('detail').classList.contains('on')||$('viewer').classList.contains('on')) return; - if(e.key==='ArrowRight'){ if(pile[0]) commit(pile[0].el,'like'); } - else if(e.key==='ArrowLeft'){ if(pile[0]) commit(pile[0].el,'nope'); } - else if(e.key==='ArrowUp'){ if(pile[0]) commit(pile[0].el,'maybe'); } - else if(e.key==='Backspace'){ undo(); } + const t=e.target; + if(t && (t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.tagName==='SELECT'||t.isContentEditable)) return; + const k=e.key; + if(k==='Escape'){ + if($('viewer').classList.contains('on')) vWalkBack(); // viewer → dossier (when reached from it) → deck + else if($('detail').classList.contains('on')) closeDetail(); + return; // Esc never forges or navigates tabs + } + if(k==='ArrowDown'){ e.preventDefault(); undo(); return; } + if(k==='Backspace'){ undo(); return; } + if(k==='ArrowLeft'||k==='ArrowRight'||k==='ArrowUp'){ + e.preventDefault(); + if($('detail').classList.contains('on')||$('viewer').classList.contains('on')) return; + const el=pile[0]&&pile[0].el; + if(el) commit(el, k==='ArrowLeft'?'nope':k==='ArrowRight'?'like':'maybe'); + } }); /* ---------- boot ---------- */