| import { useState } from 'react'; |
| import { Maximize2, Minimize2, Navigation } from 'lucide-react'; |
| import { useAppStore } from '../store'; |
|
|
| export const Minimap = () => { |
| const { images, pan, zoom, setPan, isBrowserOpen, isLibraryOpen, isSettingsOpen } = useAppStore(); |
| const [isCollapsed, setIsCollapsed] = useState(false); |
| if (images.length === 0) return null; |
|
|
| let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; |
| images.forEach(img => { |
| minX = Math.min(minX, img.x); |
| minY = Math.min(minY, img.y); |
| maxX = Math.max(maxX, img.x + img.width); |
| maxY = Math.max(maxY, img.y + img.height); |
| }); |
|
|
| const contentW = Math.max(1, maxX - minX); |
| const contentH = Math.max(1, maxY - minY); |
| const pad = Math.max(200, Math.max(contentW, contentH) * 0.15); |
| minX -= pad; minY -= pad; maxX += pad; maxY += pad; |
| const width = Math.max(1, maxX - minX); |
| const height = Math.max(1, maxY - minY); |
|
|
| const MAP_W = 160; |
| const MAP_H = 120; |
| const scale = Math.min(MAP_W / width, MAP_H / height); |
| const ox = (MAP_W - width * scale) / 2; |
| const oy = (MAP_H - height * scale) / 2; |
|
|
| const vpX = -pan.x / zoom; |
| const vpY = -pan.y / zoom; |
| const vpW = window.innerWidth / zoom; |
| const vpH = window.innerHeight / zoom; |
|
|
| const safeRight = isSettingsOpen ? 24 : isBrowserOpen ? 24 : 20; |
| const safeLeftPanel = isLibraryOpen ? 8 : 20; |
| const wrapperStyle: React.CSSProperties = { |
| position: 'fixed', |
| right: safeRight, |
| bottom: 20, |
| zIndex: 45, |
| maxWidth: 'calc(100vw - 40px)', |
| maxHeight: 'calc(100vh - 40px)', |
| }; |
|
|
| const handleClick = (e: React.PointerEvent<HTMLDivElement>) => { |
| const rect = e.currentTarget.getBoundingClientRect(); |
| const mx = e.clientX - rect.left; |
| const my = e.clientY - rect.top; |
| const worldX = (mx - ox) / scale + minX; |
| const worldY = (my - oy) / scale + minY; |
| setPan({ x: -worldX * zoom + window.innerWidth / 2, y: -worldY * zoom + window.innerHeight / 2 }); |
| }; |
|
|
| if (isCollapsed) { |
| return ( |
| <div style={wrapperStyle}> |
| <button className="w-9 h-9 bg-[#1C1C1E]/90 border border-[#3A3A3E] rounded-xl shadow-2xl backdrop-blur flex items-center justify-center text-[#A0A0A0] hover:text-white hover:border-[#0A84FF]/60 transition-colors" onClick={() => setIsCollapsed(false)} title="Show navigator"> |
| <Navigation size={15} /> |
| </button> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div style={wrapperStyle} className="w-[160px] bg-[#1C1C1E]/92 border border-[#3A3A3E] rounded-xl shadow-2xl overflow-hidden backdrop-blur-md opacity-70 hover:opacity-100 transition-opacity pointer-events-auto"> |
| <div className="h-7 px-2.5 flex items-center justify-between border-b border-[#3A3A3E]/70 bg-black/20"> |
| <div className="flex items-center gap-1.5 text-[10px] font-semibold text-[#A0A0A0] uppercase tracking-wider"> |
| <Navigation size={11} /> Navigator |
| </div> |
| <button className="w-5 h-5 hover:bg-white/10 rounded flex items-center justify-center text-white/60 hover:text-white" onClick={e => { e.stopPropagation(); setIsCollapsed(true); }} title="Collapse navigator"> |
| <Minimize2 size={10} /> |
| </button> |
| </div> |
| <div className="relative cursor-crosshair bg-[#111113]" style={{ width: MAP_W, height: MAP_H }} onPointerDown={handleClick}> |
| {images.map(img => ( |
| <div key={img.id} className="absolute bg-[#8A8A8C]/55 rounded-[1px]" style={{ left: ox + (img.x - minX) * scale, top: oy + (img.y - minY) * scale, width: Math.max(2, img.width * scale), height: Math.max(2, img.height * scale) }} /> |
| ))} |
| <div className="absolute border border-[#0A84FF] bg-[#0A84FF]/15 pointer-events-none rounded-[2px]" style={{ left: ox + (vpX - minX) * scale, top: oy + (vpY - minY) * scale, width: Math.max(8, vpW * scale), height: Math.max(8, vpH * scale) }} /> |
| </div> |
| </div> |
| ); |
| }; |
|
|