musealpha / src /components /Minimap.tsx
asdf98's picture
fix: minimap pinned bottom-right with safe padding; no top-left overflow
9d34bb7 verified
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>
);
};