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>
58 lines
2.1 KiB
TypeScript
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)}>×</button>
|
|
</div>
|
|
<div className="ml-window-content">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|