feat(v1.1): items 1-3 — keyboard map, info-first tap flow, photos = fit + blurred backdrop
Item 1: ArrowLeft/Right/Up drive nope/like/maybe through commit(); ArrowDown and Backspace (kept alias) drive undo(); Esc closes the top open sheet only (viewer -> dossier -> deck, inert on report). Text inputs/TEXTAREA/SELECT/ contentEditable swallow keys; arrow defaults are prevented (no page scroll). Item 2: card center tap now opens the dossier (bike data first); side taps keep flipping photos; the direct card -> viewer door is retired. The deck dossier records its pile entry; the hero tap opens the viewer with a backToDetail chain so Esc walks viewer -> dossier -> deck (repeatable). Item 3: deck card + dossier hero photos switch from cover to contain over a soft blurred backdrop of the same image (option C, demo panel C); thumb and fullscreen viewer untouched; nophoto paths render no backdrop.
This commit is contained in:
@@ -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);
|
.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}
|
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}
|
.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 .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}
|
.card .info{position:absolute;left:16px;right:16px;bottom:14px;z-index:3}
|
||||||
.chips{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap}
|
.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{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}
|
.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}
|
.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}
|
.dhero{position:relative;height:34%;min-height:190px;flex:none;background:#17171c;touch-action:none;overflow:hidden}
|
||||||
.dphoto{width:100%;height:100%;object-fit:cover;display:block}
|
.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}
|
.dphoto.ph{opacity:.25}
|
||||||
.dherograd{position:absolute;left:0;right:0;bottom:0;height:70px;background:linear-gradient(transparent,rgba(15,15,19,.95))}
|
.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}
|
.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
|
|||||||
<div class="detail" id="detail">
|
<div class="detail" id="detail">
|
||||||
<div class="dgrab" id="dgrab"><i></i></div>
|
<div class="dgrab" id="dgrab"><i></i></div>
|
||||||
<button class="dclose" id="dclose">⌄</button>
|
<button class="dclose" id="dclose">⌄</button>
|
||||||
<div class="dhero"><img class="dphoto" id="dphoto" alt=""><div class="dherograd"></div></div>
|
<div class="dhero"><div class="dblur" id="dblur"></div><img class="dphoto" id="dphoto" alt=""><div class="dherograd"></div></div>
|
||||||
<div class="dbody" id="dbody">
|
<div class="dbody" id="dbody">
|
||||||
<div class="dtitle" id="dtitle"></div>
|
<div class="dtitle" id="dtitle"></div>
|
||||||
<div class="drow"><div class="dprice" id="dprice"></div><div class="dsub" id="dsub"></div></div>
|
<div class="drow"><div class="dprice" id="dprice"></div><div class="dsub" id="dsub"></div></div>
|
||||||
@@ -509,8 +511,10 @@ function segHTML(n,cur){
|
|||||||
if(!n||n<2) return '';
|
if(!n||n<2) return '';
|
||||||
return `<div class="segments">${segBars(n,cur)}</div>`;
|
return `<div class="segments">${segBars(n,cur)}</div>`;
|
||||||
}
|
}
|
||||||
|
function escAttr(s){ return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,'''); }
|
||||||
function phwrapHTML(src){
|
function phwrapHTML(src){
|
||||||
return `<div class="phwrap">${src?`<img class="photo" src="${src}" alt="" draggable="false">`:`<div class="nophoto">Photo pending — thumbnail backfill queued</div>`}</div>`;
|
const img=src?`<div class="phblur" style="background-image:url('${escAttr(src)}')"></div><img class="photo" src="${escAttr(src)}" alt="" draggable="false">`:`<div class="nophoto">Photo pending — thumbnail backfill queued</div>`;
|
||||||
|
return `<div class="phwrap">${img}</div>`;
|
||||||
}
|
}
|
||||||
function attachPhError(entry){
|
function attachPhError(entry){
|
||||||
const wrap=entry.el.querySelector('.phwrap');
|
const wrap=entry.el.querySelector('.phwrap');
|
||||||
@@ -751,27 +755,34 @@ function upH(e){
|
|||||||
else if(dy<-140) commit(el,'maybe');
|
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); }
|
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.
|
/* §11.4 v1.1 tap zones: sides flip (clamped), CENTER = dossier (bike data
|
||||||
The old tap-anywhere -> dossier gesture is retired (ⓘ stays the door). */
|
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){
|
function tapZone(entry,e){
|
||||||
const n=nPhotos(entry.d);
|
|
||||||
if(n<1 || !entry.d.img) return;
|
|
||||||
const r=stage.getBoundingClientRect();
|
const r=stage.getBoundingClientRect();
|
||||||
const rel=e.clientX-r.left, w=r.width||360;
|
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});
|
if(rel>=w/3 && rel<=w*2/3){
|
||||||
else flipCard(entry, rel<w/3?-1:1);
|
dEntry=entry; // live pile entry (cur photo index)
|
||||||
|
openDetail(entry.d,{type:'deck'});
|
||||||
|
}else{
|
||||||
|
flipCard(entry, rel<w/3?-1:1); // no-ops on single/no-photo cards
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- docs/mockup core: fullscreen dossier ---------- */
|
/* ---------- docs/mockup core: fullscreen dossier ---------- */
|
||||||
let dCtx=null;
|
let dCtx=null;
|
||||||
let dData=null; // B1: dossier payload (hero -> viewer)
|
let dData=null; // B1: dossier payload (hero -> viewer)
|
||||||
|
let dEntry=null; // v1.1: pile entry when opened from the deck
|
||||||
function openDetail(data,ctx){
|
function openDetail(data,ctx){
|
||||||
dCtx=ctx||null;
|
dCtx=ctx||null;
|
||||||
dData=data;
|
dData=data;
|
||||||
|
dEntry=null; // only tapZone re-sets it (deck path)
|
||||||
const ph=$('dphoto');
|
const ph=$('dphoto');
|
||||||
if(data.img){ ph.src=data.img; ph.classList.remove('ph'); }
|
const blur=$('dblur');
|
||||||
else { ph.removeAttribute('src'); ph.classList.add('ph'); }
|
if(data.img){ ph.src=data.img; ph.classList.remove('ph'); blur.style.backgroundImage='url("'+data.img+'")'; }
|
||||||
ph.onerror=()=>{ ph.removeAttribute('src'); ph.classList.add('ph'); };
|
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||'';
|
$('dtitle').textContent=data.title||'';
|
||||||
$('dprice').textContent=priceFor(data);
|
$('dprice').textContent=priceFor(data);
|
||||||
$('dsub').textContent=subFor(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').onclick=closeDetail;
|
||||||
$('dclose').addEventListener('pointerdown',e=>e.stopPropagation());
|
$('dclose').addEventListener('pointerdown',e=>e.stopPropagation());
|
||||||
/* pull-down-to-close (grabber bar + photo) */
|
/* 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;
|
function dEndH(e){ if(dDragY===null) return; const dy=Math.max(0,e.clientY-dDragY); dDragY=null;
|
||||||
if(dy>110){ closeDetail(); }
|
if(dy>110){ closeDetail(); }
|
||||||
else { const d=$('detail'); d.style.transition='transform .25s cubic-bezier(.2,.8,.3,1)'; d.style.transform='';
|
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
|
if(e.type==='pointerup' && dy<=6 && e.currentTarget&&e.currentTarget.classList
|
||||||
&& e.currentTarget.classList.contains('dhero') && dData&&dData.img){
|
&& 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); });
|
[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){
|
function dAction(a){
|
||||||
@@ -881,6 +894,19 @@ function closeViewer(){
|
|||||||
v.style.transition=''; v.style.transform='';
|
v.style.transition=''; v.style.transform='';
|
||||||
vCtx=null; vPtrs.clear(); vPinch=null; vDrag=null;
|
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);
|
$('vback').addEventListener('click',closeViewer);
|
||||||
$('vclose').onclick=closeViewer;
|
$('vclose').onclick=closeViewer;
|
||||||
$('vclose').addEventListener('pointerdown',e=>e.stopPropagation());
|
$('vclose').addEventListener('pointerdown',e=>e.stopPropagation());
|
||||||
@@ -1260,13 +1286,28 @@ document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{
|
|||||||
else if(name==='trends') fetchTrends();
|
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=>{
|
document.addEventListener('keydown',e=>{
|
||||||
if(e.target.tagName==='INPUT'||$('detail').classList.contains('on')||$('viewer').classList.contains('on')) return;
|
const t=e.target;
|
||||||
if(e.key==='ArrowRight'){ if(pile[0]) commit(pile[0].el,'like'); }
|
if(t && (t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.tagName==='SELECT'||t.isContentEditable)) return;
|
||||||
else if(e.key==='ArrowLeft'){ if(pile[0]) commit(pile[0].el,'nope'); }
|
const k=e.key;
|
||||||
else if(e.key==='ArrowUp'){ if(pile[0]) commit(pile[0].el,'maybe'); }
|
if(k==='Escape'){
|
||||||
else if(e.key==='Backspace'){ undo(); }
|
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 ---------- */
|
/* ---------- boot ---------- */
|
||||||
|
|||||||
Reference in New Issue
Block a user