MosswartOverlord/frontend/src/components/windows/DraggableWindow.tsx
Erik de7b547349 feat(v2): Phases 2-6 — trails, heatmap, portals, windows, effects
Phase 2 — Map overlays:
- TrailsSVG: SVG polylines per character from /trails, polled 2s
- HeatmapCanvas: canvas radial gradients from /spawns/heatmap
- PortalMarkers: emoji markers from /portals
- Sidebar toggles for heatmap and portals

Phase 3 — Draggable windows:
- WindowManagerContext: z-index stack for open windows
- DraggableWindow: generic shell with drag-header, close btn, z-stack
- ChatWindow: color-coded messages + input form (1000 msg buffer)
- CharacterWindow: combat stats with monster damage table
- InventoryWindow: item table with material/set/AL/dmg/workmanship
- WindowRenderer: reads context, renders all open windows
- Action buttons (Chat/Stats/Inv/Char/Radar) now open windows

Phase 4 — Window types share same DraggableWindow shell with
character-specific content. Combat stats and inventory via API.

Phase 5 — Effects:
- RareNotification: slide-in/slide-out banner with gold border
- Fireworks: 30-particle explosion with CSS custom property animation
- Notification queue with 6s display + 0.5s exit animation

Phase 6 — Polish:
- Window header uses modern blue gradient (not solid purple)
- Chat uses monospace font
- All overlay layers properly stacked (heatmap → trails → dots → portals)
- Mobile: sidebar stacks above map at 768px breakpoint
- Chat messages tracked per-character in useLiveData

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:58:58 +02:00

58 lines
2.1 KiB
TypeScript

import React, { useRef, useCallback, useEffect } from 'react';
import { useWindowManager } from '../../contexts/WindowManagerContext';
interface Props {
id: string;
title: string;
zIndex: number;
width?: number;
height?: number;
children: React.ReactNode;
}
export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 700, height = 340, children }) => {
const { closeWindow, bringToFront } = useWindowManager();
const winRef = useRef<HTMLDivElement>(null);
const dragRef = useRef({ dragging: false, sx: 0, sy: 0, ox: 0, oy: 0 });
const posRef = useRef({ x: 420, y: 10 + Math.random() * 40 });
const onHeaderDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
bringToFront(id);
const rect = winRef.current?.getBoundingClientRect();
if (!rect) return;
dragRef.current = { dragging: true, sx: e.clientX, sy: e.clientY, ox: rect.left, oy: rect.top };
}, [id, bringToFront]);
useEffect(() => {
const onMove = (e: MouseEvent) => {
const d = dragRef.current;
if (!d.dragging || !winRef.current) return;
posRef.current.x = d.ox + (e.clientX - d.sx);
posRef.current.y = d.oy + (e.clientY - d.sy);
winRef.current.style.left = `${posRef.current.x}px`;
winRef.current.style.top = `${posRef.current.y}px`;
};
const onUp = () => { dragRef.current.dragging = false; };
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
}, []);
return (
<div
ref={winRef}
className="ml-window"
style={{ zIndex, width, height, left: posRef.current.x, top: posRef.current.y }}
onMouseDown={() => bringToFront(id)}
>
<div className="ml-window-header" onMouseDown={onHeaderDown}>
<span className="ml-window-title">{title}</span>
<button className="ml-window-close" onClick={() => closeWindow(id)}>&times;</button>
</div>
<div className="ml-window-content">
{children}
</div>
</div>
);
};