// Lightbox — full-screen photo viewer w/ zoom, before/after, EXIF, develop panel trigger
function Lightbox({ items, index, onClose, onNav, onPick, onReject, onRate, onColor, devOpen, setDevOpen, edits, onEditChange, onEditReset, showExif, setShowExif, baMode, setBaMode, zoom, setZoom, exifCache, readOnly }) {
const f = items[index];
const [screenW, setScreenW] = React.useState(window.innerWidth);
React.useEffect(() => {
const fn = () => setScreenW(window.innerWidth);
window.addEventListener('resize', fn);
return () => window.removeEventListener('resize', fn);
}, []);
const isMobile = screenW < 768;
React.useEffect(() => {
if (!f) return;
const onKey = (e) => {
if (e.key === 'Escape') { onClose(); return; }
if (e.key === 'ArrowRight') onNav(1);
if (e.key === 'ArrowLeft') onNav(-1);
if (!readOnly) {
if (e.key === 'p' || e.key === 'P') onPick();
if (e.key === 'x' || e.key === 'X') onReject();
if (e.key >= '1' && e.key <= '5') onRate(parseInt(e.key));
if (e.key === '0') onRate(0);
if (e.key === 'e' || e.key === 'E') setDevOpen(!devOpen);
}
if (e.key === 'i' || e.key === 'I') setShowExif(!showExif);
if (e.key === 'z' || e.key === 'Z') setZoom(!zoom);
if (e.key === '\\') setBaMode(!baMode);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [f, index, onNav, showExif, zoom, devOpen, baMode, readOnly]);
if (!f) return null;
const hasEdits = edits && Object.values(edits).some(v => v !== 0);
const exif = exifCache && exifCache[f.filename];
// Bottom bar height: filmstrip (48px) + mobile actions (52px on mobile)
const bottomPad = isMobile ? (readOnly ? 56 : 108) : 80;
return (
{/* ── Top bar ── */}
e.stopPropagation()}>
{/* Counter */}
{index+1}
/
{items.length}
{/* Filename + pills */}
{f.filename}
{f.file_type}
{f.color &&
}
{hasEdits &&
Edited}
{/* Desktop-only controls */}
{!isMobile && <>
{!readOnly && <>
Pick
Reject
>}
setShowExif(!showExif)} icon={} title="EXIF (I)"/>
setZoom(!zoom)} title="Zoom (Z)">
{zoom ? '100%' : 'FIT'}
setBaMode(!baMode)} icon={} title="Before/After (\)"/>
{!readOnly && (
setDevOpen(!devOpen)} icon={} title="Develop (E)">Develop
)}
} title="Download"
onClick={() => window.open(f.download_url || `/download/${encodeURIComponent(f.filename)}`, '_blank')}/>
>}
}/>
{/* ── Image area ── */}
e.stopPropagation()}>
{baMode ? (
) : (
setZoom(!zoom)}
/>
)}
{/* ── EXIF overlay ── */}
{showExif && (
e.stopPropagation()} style={{
position: 'absolute', bottom: bottomPad + 8, left: '50%', transform: 'translateX(-50%)',
background: 'rgba(10,10,12,0.88)', backdropFilter: 'blur(14px)',
border: '1px solid var(--line-2)', borderRadius: 8,
padding: '10px 20px', display: 'flex', gap: isMobile ? 16 : 28,
animation: 'fade-up 180ms ease', maxWidth: 'calc(100vw - 40px)', flexWrap: 'wrap',
}}>
{exif ? <>
{exif.camera && }
{exif.shutter && }
{exif.aperture && }
{exif.iso && }
{exif.focal_length && }
{exif.width && exif.height && }
{exif.datetime && }
> : (
Loading EXIF…
)}
)}
{/* ── Mobile action bar (above filmstrip) ── */}
{isMobile && !readOnly && (
e.stopPropagation()} style={{
position: 'absolute', bottom: 52, left: 0, right: 0, zIndex: 5,
padding: '8px 14px',
background: 'rgba(5,5,7,0.85)',
display: 'flex', alignItems: 'center', gap: 8,
}}>
{/* Pick / Reject */}
Pick
Reject
{/* Stars */}
{/* Download */}
} title="Download"
onClick={() => window.open(f.download_url || `/download/${encodeURIComponent(f.filename)}`, '_blank')}/>
{/* EXIF toggle */}
setShowExif(!showExif)} icon={}/>
)}
{/* ── Filmstrip ── */}
e.stopPropagation()} style={{
position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 5,
height: 52, padding: '8px 16px 10px', overflowX: 'auto',
background: 'linear-gradient(to top, rgba(5,5,7,0.9), transparent)',
display: 'flex', gap: 4, alignItems: 'flex-end',
}}>
{items.map((it, i) => (
))}
);
}
function navArrowStyle(side, isMobile) {
return {
position: 'absolute', [side]: isMobile ? 8 : 16, top: '50%', transform: 'translateY(-50%)',
width: isMobile ? 34 : 38, height: isMobile ? 34 : 38, borderRadius: '50%',
background: 'rgba(10,10,12,0.7)', color: 'var(--ink-1)',
border: '1px solid var(--line-2)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
backdropFilter: 'blur(8px)', zIndex: 4,
};
}
function ExifItem({ label, val, mono }) {
return (
{label}
{val}
);
}
function ColorPicker({ color, onColor }) {
const colors = [null, 'red', 'yellow', 'green', 'blue', 'purple'];
return (
{colors.map(c => (
onColor(c)}/>
))}
);
}
function BeforeAfterView({ file, edits }) {
const [pos, setPos] = React.useState(50);
const ref = React.useRef(null);
const dragging = React.useRef(false);
const imgStyle = { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' };
const calcPos = (clientX) => {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
setPos(Math.max(0, Math.min(100, ((clientX - r.left) / r.width) * 100)));
};
React.useEffect(() => {
const onMouseMove = (e) => { if (dragging.current) calcPos(e.clientX); };
const onMouseUp = () => { dragging.current = false; };
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
return () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
}, []);
return (
{ e.preventDefault(); dragging.current = true; calcPos(e.clientX); }}
onTouchStart={(e) => { e.preventDefault(); calcPos(e.touches[0].clientX); }}
onTouchMove={(e) => { e.preventDefault(); calcPos(e.touches[0].clientX); }}
>
{/* AFTER — with edits applied, full underneath */}

{/* BEFORE — original, clipped to left of divider */}
{/* Labels */}
BEFORE
AFTER
{/* Divider */}
);
}
function developFilter(edits) {
if (!edits) return 'none';
const { exposure=0, contrast=0, highlights=0, shadows=0, temperature=0, tint=0, vibrance=0, saturation=0, sharpness=0 } = edits;
const brightness = 1 + exposure * 0.14;
const c = 1 + contrast * 0.005;
const sat = 1 + saturation * 0.006 + vibrance * 0.004;
const hue = temperature * 0.15;
return `brightness(${brightness}) contrast(${c}) saturate(${sat}) hue-rotate(${hue}deg)`;
}
Object.assign(window, { Lightbox, developFilter });