Files
mNetVisualizer/frontend/src/DraggablePanel.jsx
T

89 lines
3.6 KiB
React

import React, { useState, useRef, useCallback, useEffect } from 'react';
const MIN_W = 220;
const MIN_H = 160;
export function DraggablePanel({ id, title, badge, extra, onClose, children, defaultPos }) {
const [pos, setPos] = useState(() => {
try { const s = localStorage.getItem(`panel-pos-${id}`); return s ? JSON.parse(s) : defaultPos; }
catch { return defaultPos; }
});
const [size, setSize] = useState(() => {
try { const s = localStorage.getItem(`panel-size-${id}`); return s ? JSON.parse(s) : { w: 272, h: 480 }; }
catch { return { w: 272, h: 480 }; }
});
const savePos = (p) => { try { localStorage.setItem(`panel-pos-${id}`, JSON.stringify(p)); } catch {} };
const saveSize = (s) => { try { localStorage.setItem(`panel-size-${id}`, JSON.stringify(s)); } catch {} };
const dragRef = useRef(null);
const resizeRef = useRef(null);
const panelRef = useRef(null);
// ── Drag ──────────────────────────────────────────────────────────────────
const onDragMouseDown = useCallback((e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
dragRef.current = { startX: e.clientX, startY: e.clientY, origX: pos.x, origY: pos.y };
}, [pos]);
// ── Resize ────────────────────────────────────────────────────────────────
const onResizeMouseDown = useCallback((e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
resizeRef.current = { startX: e.clientX, startY: e.clientY, origW: size.w, origH: size.h };
}, [size]);
useEffect(() => {
const onMouseMove = (e) => {
if (dragRef.current) {
const d = dragRef.current;
const nx = Math.max(0, Math.min(window.innerWidth - size.w, d.origX + (e.clientX - d.startX)));
const ny = Math.max(0, Math.min(window.innerHeight - 60, d.origY + (e.clientY - d.startY)));
const p = { x: nx, y: ny };
setPos(p);
savePos(p);
}
if (resizeRef.current) {
const d = resizeRef.current;
const nw = Math.max(MIN_W, d.origW + (e.clientX - d.startX));
const nh = Math.max(MIN_H, d.origH + (e.clientY - d.startY));
const s = { w: nw, h: nh };
setSize(s);
saveSize(s);
}
};
const onMouseUp = () => { dragRef.current = null; resizeRef.current = null; };
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); };
}, [size.w]);
return (
<div
ref={panelRef}
className="floating-panel panel"
style={{ left: pos.x, top: pos.y, width: size.w, height: size.h }}
onClick={e => e.stopPropagation()}
>
{/* Header — drag handle */}
<div className="fp-header" onMouseDown={onDragMouseDown}>
<span className="fp-title">{title}</span>
{badge != null && <span className="tab-count">{badge}</span>}
{extra}
<button className="close-btn" onMouseDown={e=>e.stopPropagation()} onClick={onClose}>✕</button>
</div>
{/* Content */}
<div className="fp-body">
{children}
</div>
{/* Resize handle — bottom-right corner */}
<div className="fp-resize" onMouseDown={onResizeMouseDown}>⌟</div>
</div>
);
}