Add image lightbox for item thumbnails and kit parts
This commit is contained in:
@@ -25,6 +25,14 @@ export function ItemCard({ item, onEdit, onRemove }: Props) {
|
|||||||
|
|
||||||
const [hoverImg, setHoverImg] = useState<string | null>(null)
|
const [hoverImg, setHoverImg] = useState<string | null>(null)
|
||||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
|
||||||
|
const [lightboxImg, setLightboxImg] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lightboxImg) return
|
||||||
|
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') setLightboxImg(null) }
|
||||||
|
document.addEventListener('keydown', onKey)
|
||||||
|
return () => document.removeEventListener('keydown', onKey)
|
||||||
|
}, [lightboxImg])
|
||||||
|
|
||||||
// Preload part images into memCache so resolveImageSync works on hover
|
// Preload part images into memCache so resolveImageSync works on hover
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,7 +60,12 @@ export function ItemCard({ item, onEdit, onRemove }: Props) {
|
|||||||
>
|
>
|
||||||
{/* Thumbnail */}
|
{/* Thumbnail */}
|
||||||
{thumbnail ? (
|
{thumbnail ? (
|
||||||
<img src={thumbnail} alt={item.name} className="h-16 w-16 shrink-0 rounded object-cover" />
|
<img
|
||||||
|
src={thumbnail}
|
||||||
|
alt={item.name}
|
||||||
|
className="h-16 w-16 shrink-0 rounded object-cover cursor-zoom-in"
|
||||||
|
onClick={() => setLightboxImg(thumbnail)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="flex h-16 w-16 shrink-0 items-center justify-center rounded text-2xl"
|
className="flex h-16 w-16 shrink-0 items-center justify-center rounded text-2xl"
|
||||||
@@ -110,10 +123,11 @@ export function ItemCard({ item, onEdit, onRemove }: Props) {
|
|||||||
<li
|
<li
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className="flex items-center justify-between gap-2 text-xs"
|
className="flex items-center justify-between gap-2 text-xs"
|
||||||
style={{ color: 'var(--color-grey)', cursor: p.image ? 'default' : undefined }}
|
style={{ color: 'var(--color-grey)', cursor: p.image ? 'zoom-in' : undefined }}
|
||||||
onMouseEnter={p.image ? e => { setHoverImg(resolveImageSync(p.image) ?? null); setMousePos({ x: e.clientX, y: e.clientY }) } : undefined}
|
onMouseEnter={p.image ? e => { setHoverImg(resolveImageSync(p.image) ?? null); setMousePos({ x: e.clientX, y: e.clientY }) } : undefined}
|
||||||
onMouseMove={p.image ? e => setMousePos({ x: e.clientX, y: e.clientY }) : undefined}
|
onMouseMove={p.image ? e => setMousePos({ x: e.clientX, y: e.clientY }) : undefined}
|
||||||
onMouseLeave={p.image ? () => setHoverImg(null) : undefined}
|
onMouseLeave={p.image ? () => setHoverImg(null) : undefined}
|
||||||
|
onClick={p.image ? () => { const r = resolveImageSync(p.image); if (r) setLightboxImg(r) } : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className="shrink-0 rounded px-1.5 py-0.5" style={{ background: 'var(--color-bg2)', color: 'var(--color-grey)' }}>
|
<span className="shrink-0 rounded px-1.5 py-0.5" style={{ background: 'var(--color-bg2)', color: 'var(--color-grey)' }}>
|
||||||
@@ -141,15 +155,32 @@ export function ItemCard({ item, onEdit, onRemove }: Props) {
|
|||||||
{/* Hover image preview */}
|
{/* Hover image preview */}
|
||||||
{hoverImg && (
|
{hoverImg && (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none fixed z-50 rounded-lg border shadow-lg overflow-hidden"
|
className="fixed z-50 rounded-lg border shadow-lg overflow-hidden cursor-zoom-in"
|
||||||
style={{
|
style={{
|
||||||
left: mousePos.x + 16,
|
left: mousePos.x + 16,
|
||||||
top: mousePos.y + 16,
|
top: mousePos.y + 16,
|
||||||
borderColor: 'var(--color-bg3)',
|
borderColor: 'var(--color-bg3)',
|
||||||
background: 'var(--color-bg1)',
|
background: 'var(--color-bg1)',
|
||||||
}}
|
}}
|
||||||
|
onClick={() => setLightboxImg(hoverImg)}
|
||||||
>
|
>
|
||||||
<img src={hoverImg} alt="" className="block h-48 w-48 object-cover" />
|
<img src={hoverImg} alt="" className="pointer-events-none block h-48 w-48 object-cover" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lightbox */}
|
||||||
|
{lightboxImg && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 cursor-zoom-out"
|
||||||
|
style={{ background: 'rgba(0,0,0,0.8)' }}
|
||||||
|
onClick={() => setLightboxImg(null)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={lightboxImg}
|
||||||
|
alt=""
|
||||||
|
className="max-h-full max-w-full rounded-lg object-contain shadow-2xl"
|
||||||
|
style={{ maxHeight: '90vh', maxWidth: '90vw' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user