89 lines
3.6 KiB
React
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>
|
|
);
|
|
}
|