Spaces:
Running
Running
File size: 5,196 Bytes
5f3e9f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | import { Check, ChevronLeft, ChevronRight, Copy, Download, ExternalLink, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useFocusTrap } from '../hooks/useFocusTrap'
import { useToast } from '../store/toast'
interface Props {
kind: 'html' | 'image' | 'video'
src: string
title: string
subtitle?: string
onClose: () => void
onPrevious?: () => void
onNext?: () => void
}
export default function AssetPreviewModal({
kind,
src,
title,
subtitle,
onClose,
onPrevious,
onNext,
}: Props) {
const dialogRef = useFocusTrap<HTMLDivElement>(true)
const [copyState, setCopyState] = useState<'idle' | 'copying' | 'ok'>('idle')
const toast = useToast()
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (kind === 'image' && e.key === 'ArrowLeft') onPrevious?.()
if (kind === 'image' && e.key === 'ArrowRight') onNext?.()
}
window.addEventListener('keydown', onKey)
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
window.removeEventListener('keydown', onKey)
document.body.style.overflow = prev
}
}, [kind, onClose, onNext, onPrevious])
const onCopyHtml = async () => {
if (kind !== 'html') return
setCopyState('copying')
try {
const res = await fetch(src)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const text = await res.text()
await navigator.clipboard.writeText(text)
setCopyState('ok')
toast.push({ variant: 'success', message: 'HTML copied to clipboard.' })
window.setTimeout(() => setCopyState('idle'), 1500)
} catch (e) {
setCopyState('idle')
toast.push({
variant: 'error',
title: 'Copy failed',
message: e instanceof Error ? e.message : String(e),
})
}
}
const downloadName = title.split('/').pop() ?? title
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-slate-950/70 backdrop-blur-sm" onClick={onClose} aria-hidden />
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label={title}
tabIndex={-1}
className="glass-strong relative flex h-full max-h-[92vh] w-full max-w-6xl flex-col overflow-hidden"
>
<div className="flex items-start justify-between gap-4 border-b border-slate-200 px-4 py-3 dark:border-white/10">
<div className="min-w-0">
<div className="truncate font-display text-sm font-semibold text-slate-900 dark:text-slate-50">
{title}
</div>
{subtitle && (
<div className="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
{subtitle}
</div>
)}
</div>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{kind === 'image' && onPrevious && (
<button type="button" className="btn-secondary btn-sm" onClick={onPrevious}>
<ChevronLeft size={12} /> Previous
</button>
)}
{kind === 'image' && onNext && (
<button type="button" className="btn-secondary btn-sm" onClick={onNext}>
Next <ChevronRight size={12} />
</button>
)}
{kind === 'html' && (
<button
type="button"
className="btn-secondary btn-sm"
onClick={onCopyHtml}
disabled={copyState === 'copying'}
>
{copyState === 'ok' ? <Check size={14} /> : <Copy size={14} />}
{copyState === 'ok' ? 'Copied' : copyState === 'copying' ? 'Copying...' : 'Copy HTML'}
</button>
)}
<a href={src} download={downloadName} className="btn-secondary btn-sm">
<Download size={14} /> Download
</a>
<a href={src} target="_blank" rel="noopener noreferrer" className="btn-secondary btn-sm">
<ExternalLink size={14} /> Open
</a>
<button type="button" className="btn-ghost !px-2" onClick={onClose} aria-label="Close preview">
<X size={16} />
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto bg-slate-50 p-4 dark:bg-slate-950/40">
{kind === 'html' ? (
<iframe
src={src}
title={title}
sandbox="allow-same-origin allow-scripts allow-forms"
className="h-full min-h-[70vh] w-full rounded-md border-0 bg-white dark:bg-slate-950"
/>
) : kind === 'video' ? (
<video src={src} controls autoPlay className="max-h-full max-w-full rounded-md bg-black" />
) : (
<img src={src} alt={title} className="max-h-full max-w-full rounded-md object-contain shadow-lg" />
)}
</div>
</div>
</div>,
document.body,
)
}
|