| 10 | } |
| 11 | |
| 12 | export function ImageViewer({ src, path }: ImageViewerProps) { |
| 13 | const [zoom, setZoom] = useState(1); |
| 14 | const [fitMode, setFitMode] = useState<"fit" | "actual">("fit"); |
| 15 | const [error, setError] = useState(false); |
| 16 | const containerRef = useRef<HTMLDivElement>(null); |
| 17 | |
| 18 | const handleZoomIn = () => setZoom((z) => Math.min(z * 1.25, 8)); |
| 19 | const handleZoomOut = () => setZoom((z) => Math.max(z / 1.25, 0.1)); |
| 20 | const handleFitToggle = () => { |
| 21 | setFitMode((m) => (m === "fit" ? "actual" : "fit")); |
| 22 | setZoom(1); |
| 23 | }; |
| 24 | |
| 25 | const isSvg = path.endsWith(".svg"); |
| 26 | const hasTransparency = path.endsWith(".png") || path.endsWith(".gif") || |
| 27 | path.endsWith(".webp") || path.endsWith(".svg"); |
| 28 | |
| 29 | if (error) { |
| 30 | return ( |
| 31 | <div className="flex flex-col items-center justify-center h-full gap-3 text-surface-500"> |
| 32 | <ImageIcon className="w-10 h-10" /> |
| 33 | <p className="text-sm">Failed to load image</p> |
| 34 | <p className="text-xs text-surface-600">{path}</p> |
| 35 | </div> |
| 36 | ); |
| 37 | } |
| 38 | |
| 39 | return ( |
| 40 | <div className="flex flex-col h-full"> |
| 41 | {/* Toolbar */} |
| 42 | <div className="flex items-center gap-1 px-2 py-1 border-b border-surface-800 bg-surface-900/50"> |
| 43 | <button |
| 44 | onClick={handleZoomOut} |
| 45 | className="p-1 rounded text-surface-500 hover:text-surface-200 hover:bg-surface-800 transition-colors" |
| 46 | title="Zoom out" |
| 47 | > |
| 48 | <ZoomOut className="w-3.5 h-3.5" /> |
| 49 | </button> |
| 50 | <span className="text-xs text-surface-400 w-12 text-center"> |
| 51 | {Math.round(zoom * 100)}% |
| 52 | </span> |
| 53 | <button |
| 54 | onClick={handleZoomIn} |
| 55 | className="p-1 rounded text-surface-500 hover:text-surface-200 hover:bg-surface-800 transition-colors" |
| 56 | title="Zoom in" |
| 57 | > |
| 58 | <ZoomIn className="w-3.5 h-3.5" /> |
| 59 | </button> |
| 60 | <div className="w-px h-4 bg-surface-800 mx-1" /> |
| 61 | <button |
| 62 | onClick={handleFitToggle} |
| 63 | className={cn( |
| 64 | "flex items-center gap-1 px-2 py-0.5 rounded text-xs transition-colors", |
| 65 | fitMode === "fit" |
| 66 | ? "text-brand-400 bg-brand-900/30" |
| 67 | : "text-surface-500 hover:text-surface-200 hover:bg-surface-800" |
| 68 | )} |
| 69 | title={fitMode === "fit" ? "Switch to actual size" : "Switch to fit width"} |