@@ -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 ---------- */