feat(v2): remove old dashboard, add vitae + resizable windows
- Removed old Recharts dashboard view entirely (no more viewMode toggle, DashboardView lazy import, Ctrl+D shortcut) - Recharts chunk eliminated from build — bundle size reduced - Player Dashboard window: added Vitae column (red when > 0%) - ALL windows now resizable: drag bottom-right corner handle (min 300×200px). Subtle diagonal line grip indicator. - Sidebar: removed 📊 Dashboard toggle link, removed broken /quest-status.html external link (replaced by 📜 Quests window) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
938421999a
commit
a5bd659876
37 changed files with 168 additions and 178 deletions
|
|
@ -1,45 +1,10 @@
|
|||
import { useState, lazy, Suspense, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { MapLayout } from './components/map/MapLayout';
|
||||
import { useLiveData } from './hooks/useLiveData';
|
||||
import './styles/global.css';
|
||||
import './styles/map-layout.css';
|
||||
|
||||
// Lazy-load dashboard view (contains Recharts ~400KB) — only loaded when user switches to dashboard
|
||||
const DashboardView = lazy(() => import('./DashboardView'));
|
||||
|
||||
type ViewMode = 'map' | 'dashboard';
|
||||
|
||||
export default function App() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(
|
||||
() => (localStorage.getItem('v2-view') as ViewMode) || 'map'
|
||||
);
|
||||
const data = useLiveData();
|
||||
|
||||
// Ctrl+D toggles map/dashboard
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'd') {
|
||||
e.preventDefault();
|
||||
toggleView();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
|
||||
const toggleView = () => {
|
||||
const next = viewMode === 'map' ? 'dashboard' : 'map';
|
||||
setViewMode(next);
|
||||
localStorage.setItem('v2-view', next);
|
||||
};
|
||||
|
||||
if (viewMode === 'map') {
|
||||
return <MapLayout data={data} onViewToggle={toggleView} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div style={{ background: '#0d0d0d', color: '#888', padding: 40, textAlign: 'center' }}>Loading dashboard...</div>}>
|
||||
<DashboardView data={data} onViewToggle={toggleView} />
|
||||
</Suspense>
|
||||
);
|
||||
return <MapLayout data={data} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ import type { DashboardState } from '../../hooks/useLiveData';
|
|||
|
||||
interface Props {
|
||||
data: DashboardState;
|
||||
onViewToggle: () => void;
|
||||
}
|
||||
|
||||
export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
||||
export const MapLayout: React.FC<Props> = ({ data }) => {
|
||||
const getColor = usePlayerColors();
|
||||
const [showHeatmap, setShowHeatmap] = useState(false);
|
||||
const [showPortals, setShowPortals] = useState(false);
|
||||
|
|
@ -49,7 +48,6 @@ export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
|||
totalKills={data.totalKills}
|
||||
getColor={getColor}
|
||||
onSelectPlayer={handleSelectPlayer}
|
||||
onViewToggle={onViewToggle}
|
||||
showHeatmap={showHeatmap}
|
||||
showPortals={showPortals}
|
||||
onToggleHeatmap={setShowHeatmap}
|
||||
|
|
@ -57,6 +55,7 @@ export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
|||
version={version}
|
||||
selectedPlayer={selectedPlayer}
|
||||
/>
|
||||
|
||||
<MapView
|
||||
players={players}
|
||||
getColor={getColor}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ interface Props {
|
|||
totalKills: number;
|
||||
getColor: (name: string) => string;
|
||||
onSelectPlayer: (name: string) => void;
|
||||
onViewToggle: () => void;
|
||||
showHeatmap: boolean;
|
||||
showPortals: boolean;
|
||||
onToggleHeatmap: (v: boolean) => void;
|
||||
|
|
@ -22,9 +21,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export const Sidebar: React.FC<Props> = ({
|
||||
players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer, onViewToggle,
|
||||
players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer,
|
||||
showHeatmap, showPortals, onToggleHeatmap, onTogglePortals, version, selectedPlayer,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const [sortKey, setSortKey] = useState<SortKey>('name');
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
|
|
@ -81,7 +80,6 @@ export const Sidebar: React.FC<Props> = ({
|
|||
|
||||
{/* Tool links */}
|
||||
<div className="ml-tool-links">
|
||||
<span className="ml-tool-link" style={{ cursor: 'pointer' }} onClick={onViewToggle}>📊 Dashboard</span>
|
||||
<a href="/inventory.html" target="_blank" className="ml-tool-link">🔍 Inv Search</a>
|
||||
<a href="/suitbuilder.html" target="_blank" className="ml-tool-link">🛡️ Suitbuilder</a>
|
||||
<a href="/debug.html" target="_blank" className="ml-tool-link">🐛 Debug</a>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useCallback, useEffect } from 'react';
|
||||
import React, { useRef, useCallback, useEffect, useState } from 'react';
|
||||
import { useWindowManager } from '../../contexts/WindowManagerContext';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -14,7 +14,9 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
const { closeWindow, bringToFront } = useWindowManager();
|
||||
const winRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef({ dragging: false, sx: 0, sy: 0, ox: 0, oy: 0 });
|
||||
const resizeRef = useRef({ resizing: false, sx: 0, sy: 0, sw: 0, sh: 0 });
|
||||
const posRef = useRef({ x: 420, y: 10 + Math.random() * 40 });
|
||||
const [size, setSize] = useState({ w: width, h: height });
|
||||
|
||||
const onHeaderDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -24,16 +26,34 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
dragRef.current = { dragging: true, sx: e.clientX, sy: e.clientY, ox: rect.left, oy: rect.top };
|
||||
}, [id, bringToFront]);
|
||||
|
||||
const onResizeDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizeRef.current = { resizing: true, sx: e.clientX, sy: e.clientY, sw: size.w, sh: size.h };
|
||||
}, [size.w, size.h]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
// Drag
|
||||
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`;
|
||||
if (d.dragging && winRef.current) {
|
||||
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`;
|
||||
}
|
||||
// Resize
|
||||
const r = resizeRef.current;
|
||||
if (r.resizing) {
|
||||
const newW = Math.max(300, r.sw + (e.clientX - r.sx));
|
||||
const newH = Math.max(200, r.sh + (e.clientY - r.sy));
|
||||
setSize({ w: newW, h: newH });
|
||||
}
|
||||
};
|
||||
const onUp = () => {
|
||||
dragRef.current.dragging = false;
|
||||
resizeRef.current.resizing = false;
|
||||
};
|
||||
const onUp = () => { dragRef.current.dragging = false; };
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
|
||||
|
|
@ -43,7 +63,7 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
<div
|
||||
ref={winRef}
|
||||
className="ml-window"
|
||||
style={{ zIndex, width, height, left: posRef.current.x, top: posRef.current.y }}
|
||||
style={{ zIndex, width: size.w, height: size.h, left: posRef.current.x, top: posRef.current.y }}
|
||||
onMouseDown={() => bringToFront(id)}
|
||||
>
|
||||
<div className="ml-window-header" onMouseDown={onHeaderDown}>
|
||||
|
|
@ -53,6 +73,8 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
<div className="ml-window-content">
|
||||
{children}
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div className="ml-window-resize" onMouseDown={onResizeDown} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const PlayerDashboardWindow: React.FC<Props> = ({ id, zIndex, characters
|
|||
state: t.vt_state ?? 'idle',
|
||||
tapers: parseInt(t.prismatic_taper_count as string) || 0,
|
||||
hp: c.vitals?.health_percentage ?? 0,
|
||||
vitae: c.vitals?.vitae ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ export const PlayerDashboardWindow: React.FC<Props> = ({ id, zIndex, characters
|
|||
<th style={{ ...thStyle('deaths'), textAlign: 'right' }} onClick={() => toggleSort('deaths')}>Deaths{arrow('deaths')}</th>
|
||||
<th style={{ ...thStyle('uptime'), textAlign: 'right' }} onClick={() => toggleSort('uptime')}>Uptime{arrow('uptime')}</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 6px', color: '#888', fontSize: '0.65rem', fontWeight: 600, borderBottom: '1px solid #444' }}>HP%</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 6px', color: '#888', fontSize: '0.65rem', fontWeight: 600, borderBottom: '1px solid #444' }}>Vitae</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 6px', color: '#888', fontSize: '0.65rem', fontWeight: 600, borderBottom: '1px solid #444' }}>Tapers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -98,6 +100,8 @@ export const PlayerDashboardWindow: React.FC<Props> = ({ id, zIndex, characters
|
|||
<td style={{ textAlign: 'right', padding: '3px 6px', color: '#888', fontVariantNumeric: 'tabular-nums' }}>{p.uptime}</td>
|
||||
<td style={{ textAlign: 'right', padding: '3px 6px', fontVariantNumeric: 'tabular-nums',
|
||||
color: p.hp > 80 ? '#4c4' : p.hp > 40 ? '#ca0' : '#c44' }}>{p.hp.toFixed(0)}%</td>
|
||||
<td style={{ textAlign: 'right', padding: '3px 6px', fontVariantNumeric: 'tabular-nums',
|
||||
color: p.vitae > 0 ? '#f66' : '#333' }}>{p.vitae > 0 ? `${p.vitae}%` : ''}</td>
|
||||
<td style={{ textAlign: 'right', padding: '3px 6px', color: '#888', fontVariantNumeric: 'tabular-nums' }}>{p.tapers.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -528,6 +528,18 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ml-window-resize {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: nwse-resize;
|
||||
opacity: 0.3;
|
||||
background: linear-gradient(135deg, transparent 50%, #888 50%, transparent 52%, #888 65%, transparent 67%, #888 80%);
|
||||
}
|
||||
.ml-window-resize:hover { opacity: 0.6; }
|
||||
|
||||
/* ── Stats window (Grafana iframes) ───────────────────── */
|
||||
.ml-stats-controls {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue