fix: minimap pinned bottom-right with safe padding; no top-left overflow
Browse files- src/components/Minimap.tsx +84 -36
src/components/Minimap.tsx
CHANGED
|
@@ -1,36 +1,84 @@
|
|
| 1 |
-
import { useState } from 'react';
|
| 2 |
-
import { Maximize2, Minimize2 } from 'lucide-react';
|
| 3 |
-
import { useAppStore } from '../store';
|
| 4 |
-
|
| 5 |
-
export const Minimap = () => {
|
| 6 |
-
const { images, pan, zoom, setPan } = useAppStore();
|
| 7 |
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
| 8 |
-
if (images.length === 0) return null;
|
| 9 |
-
|
| 10 |
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
| 11 |
-
images.forEach(img => {
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
const
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { Maximize2, Minimize2, Navigation } from 'lucide-react';
|
| 3 |
+
import { useAppStore } from '../store';
|
| 4 |
+
|
| 5 |
+
export const Minimap = () => {
|
| 6 |
+
const { images, pan, zoom, setPan, isBrowserOpen, isLibraryOpen, isSettingsOpen } = useAppStore();
|
| 7 |
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
| 8 |
+
if (images.length === 0) return null;
|
| 9 |
+
|
| 10 |
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
| 11 |
+
images.forEach(img => {
|
| 12 |
+
minX = Math.min(minX, img.x);
|
| 13 |
+
minY = Math.min(minY, img.y);
|
| 14 |
+
maxX = Math.max(maxX, img.x + img.width);
|
| 15 |
+
maxY = Math.max(maxY, img.y + img.height);
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
const contentW = Math.max(1, maxX - minX);
|
| 19 |
+
const contentH = Math.max(1, maxY - minY);
|
| 20 |
+
const pad = Math.max(200, Math.max(contentW, contentH) * 0.15);
|
| 21 |
+
minX -= pad; minY -= pad; maxX += pad; maxY += pad;
|
| 22 |
+
const width = Math.max(1, maxX - minX);
|
| 23 |
+
const height = Math.max(1, maxY - minY);
|
| 24 |
+
|
| 25 |
+
const MAP_W = 160;
|
| 26 |
+
const MAP_H = 120;
|
| 27 |
+
const scale = Math.min(MAP_W / width, MAP_H / height);
|
| 28 |
+
const ox = (MAP_W - width * scale) / 2;
|
| 29 |
+
const oy = (MAP_H - height * scale) / 2;
|
| 30 |
+
|
| 31 |
+
const vpX = -pan.x / zoom;
|
| 32 |
+
const vpY = -pan.y / zoom;
|
| 33 |
+
const vpW = window.innerWidth / zoom;
|
| 34 |
+
const vpH = window.innerHeight / zoom;
|
| 35 |
+
|
| 36 |
+
const safeRight = isSettingsOpen ? 24 : isBrowserOpen ? 24 : 20;
|
| 37 |
+
const safeLeftPanel = isLibraryOpen ? 8 : 20;
|
| 38 |
+
const wrapperStyle: React.CSSProperties = {
|
| 39 |
+
position: 'fixed',
|
| 40 |
+
right: safeRight,
|
| 41 |
+
bottom: 20,
|
| 42 |
+
zIndex: 45,
|
| 43 |
+
maxWidth: 'calc(100vw - 40px)',
|
| 44 |
+
maxHeight: 'calc(100vh - 40px)',
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const handleClick = (e: React.PointerEvent<HTMLDivElement>) => {
|
| 48 |
+
const rect = e.currentTarget.getBoundingClientRect();
|
| 49 |
+
const mx = e.clientX - rect.left;
|
| 50 |
+
const my = e.clientY - rect.top;
|
| 51 |
+
const worldX = (mx - ox) / scale + minX;
|
| 52 |
+
const worldY = (my - oy) / scale + minY;
|
| 53 |
+
setPan({ x: -worldX * zoom + window.innerWidth / 2, y: -worldY * zoom + window.innerHeight / 2 });
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
if (isCollapsed) {
|
| 57 |
+
return (
|
| 58 |
+
<div style={wrapperStyle}>
|
| 59 |
+
<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">
|
| 60 |
+
<Navigation size={15} />
|
| 61 |
+
</button>
|
| 62 |
+
</div>
|
| 63 |
+
);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return (
|
| 67 |
+
<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">
|
| 68 |
+
<div className="h-7 px-2.5 flex items-center justify-between border-b border-[#3A3A3E]/70 bg-black/20">
|
| 69 |
+
<div className="flex items-center gap-1.5 text-[10px] font-semibold text-[#A0A0A0] uppercase tracking-wider">
|
| 70 |
+
<Navigation size={11} /> Navigator
|
| 71 |
+
</div>
|
| 72 |
+
<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">
|
| 73 |
+
<Minimize2 size={10} />
|
| 74 |
+
</button>
|
| 75 |
+
</div>
|
| 76 |
+
<div className="relative cursor-crosshair bg-[#111113]" style={{ width: MAP_W, height: MAP_H }} onPointerDown={handleClick}>
|
| 77 |
+
{images.map(img => (
|
| 78 |
+
<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) }} />
|
| 79 |
+
))}
|
| 80 |
+
<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) }} />
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
};
|