Files
Space-Game/archive/legacy-static/js/demos/starmap.js
francy51 316a44661b Restructure into pnpm monorepo with game shell, docs, and SpacetimeDB backend
- Restructure flat static prototype into pnpm workspace monorepo
- apps/game: playable shell with R3F 3D scene, HUD, SpacetimeDB connection
- apps/docs: design docs and prototypes
- apps/site: landing page
- packages/ui: shared Button and Panel primitives
- services/spacetimedb: backend module (9 tables, 11 reducers)
- Archive legacy static files to archive/legacy-static/
- Game loop: connect, undock, target, approach, dock, mine, sell
- Add pnpm-workspace.yaml, tsconfig.base.json, spacetime.json
2026-05-31 17:56:56 -04:00

1045 lines
56 KiB
JavaScript

window.GDD = window.GDD || {};
const { useState, useEffect, useRef, useCallback, useMemo } = React;
const TH = window.GDD.THREE;
/* ── Scale constants ── */
const SCALE = 0.5; // world position scale
const ORBIT_SCALE = 0.22; // orbital radius scale
const PLANET_SCALE = 0.7; // planet mesh scale
function StarMapDemo() {
const containerRef = useRef(null);
const sceneRef = useRef(null);
const [systems, setSystems] = useState([]);
const [connections, setConnections] = useState([]);
const [selected, setSelected] = useState(null);
const [destination, setDestination] = useState(null);
const [hovered, setHovered] = useState(null);
const [waypoints, setWaypoints] = useState([]);
const [route, setRoute] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [systemFilter, setSystemFilter] = useState('all');
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, target: null });
const systemMeshesRef = useRef([]);
const routeLineRef = useRef(null);
const connLinesRef = useRef([]);
const animIdRef = useRef(null);
const cameraCtrlRef = useRef(null);
const minimapCanvasRef = useRef(null);
const focusTargetRef = useRef(null);
const orbitalBodiesRef = useRef([]);
const asteroidBeltsRef = useRef([]);
/* ── Celestial data ── */
const celestialData = useMemo(() => {
return window.GDD.CONSTANTS.CELESTIAL_BODIES || {};
}, []);
const selectedCelestial = useMemo(() => {
if (!selected) return null;
return celestialData[selected.id] || null;
}, [selected, celestialData]);
/* ── Load data ── */
useEffect(() => {
window.GDD.api.getSystems().then(s => setSystems(s));
window.GDD.api.getConnections().then(c => setConnections(c));
}, []);
/* ── Camera fly-to on selection ── */
useEffect(() => {
if (!focusTargetRef.current || !cameraCtrlRef.current) return;
cameraCtrlRef.current.flyTo(focusTargetRef.current.x, focusTargetRef.current.y, focusTargetRef.current.z || 0);
focusTargetRef.current = null;
}, [selected]);
/* ── Context menu close on click / Escape / scroll ── */
useEffect(() => {
const close = () => setContextMenu(prev => ({ ...prev, visible: false }));
const onKey = (e) => { if (e.key === 'Escape') close(); };
window.addEventListener('click', close);
window.addEventListener('keydown', onKey);
return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', onKey); };
}, []);
/* ── Build 3D scene ── */
useEffect(() => {
if (systems.length === 0) return;
const container = containerRef.current;
if (!container) return;
// Cleanup previous
if (sceneRef.current) {
sceneRef.current.scene.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material) { if (child.material.map) child.material.map.dispose(); child.material.dispose(); }
});
sceneRef.current.renderer.dispose();
if (sceneRef.current.renderer.domElement.parentNode) sceneRef.current.renderer.domElement.parentNode.removeChild(sceneRef.current.renderer.domElement);
if (animIdRef.current) cancelAnimationFrame(animIdRef.current);
if (cameraCtrlRef.current) cameraCtrlRef.current.dispose();
}
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x040810, 0.0003);
const w = container.clientWidth;
const h = container.clientHeight;
const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 5000);
camera.position.set(0, 180, 220);
camera.lookAt(0, 0, 0);
const renderer = TH.createRenderer(container, { clearColor: 0x040810 });
renderer.setSize(w, h);
TH.handleResize(renderer, camera, container);
// Star field
const stars = TH.createStarField(4000, 2500);
scene.add(stars);
// Nebulae
TH.addNebula(scene, 0x22d3ee, [-100, 50, -300], 400);
TH.addNebula(scene, 0xa78bfa, [200, -80, -200], 300);
TH.addNebula(scene, 0xf0a030, [0, 100, -250], 200);
// Grid
const grid = new THREE.GridHelper(600, 30, 0x0d1520, 0x0d1520);
grid.material.transparent = true;
grid.material.opacity = 0.12;
scene.add(grid);
TH.setupSpaceLighting(scene);
// Build system meshes
systemMeshesRef.current = [];
orbitalBodiesRef.current = [];
asteroidBeltsRef.current = [];
systems.forEach(sys => {
const group = TH.createStarSystem(sys, SCALE);
scene.add(group);
systemMeshesRef.current.push(group);
// Create celestial bodies for this system
const cData = celestialData[sys.id];
if (!cData) return;
const sysPos = new THREE.Vector3(sys.x * SCALE, sys.y * SCALE, 0);
cData.bodies.forEach(body => {
if (body.type === 'belt') {
// Asteroid belt
const belt = TH.createAsteroidBelt(
body.innerOrbit * ORBIT_SCALE,
body.outerOrbit * ORBIT_SCALE,
body.count,
{ basePeriod: 20, maxInclination: 0.12 }
);
belt.position.copy(sysPos);
scene.add(belt);
asteroidBeltsRef.current.push({ belt, parentPos: sysPos });
return;
}
// Planet
const orbitR = body.orbit * ORBIT_SCALE;
const planetSize = body.size * PLANET_SCALE;
const planetMesh = TH.createPlanetMesh(planetSize, body.color, body.atmosphere);
scene.add(planetMesh);
// Orbit trail
const trail = TH.createOrbitTrail(orbitR, body.ecc, body.inc, 0x1a3050, 0.18);
trail.position.copy(sysPos);
scene.add(trail);
// Orbital body
const orbBody = new TH.OrbitalBody({
mesh: planetMesh,
parentPos: sysPos,
orbitRadius: orbitR,
eccentricity: body.ecc,
inclination: body.inc,
period: body.period,
});
// Rings
if (body.hasRings) {
const rings = TH.createRingSystem(planetSize * 1.3, planetSize * 2.1, body.color, 0.25);
planetMesh.add(rings);
}
// Moons
if (body.moons) {
body.moons.forEach(moon => {
const moonSize = moon.size * PLANET_SCALE * 0.8;
const moonMesh = TH.createPlanetMesh(moonSize, moon.color);
scene.add(moonMesh);
const moonOrbitR = moon.orbit * ORBIT_SCALE * 0.5;
const moonTrail = TH.createOrbitTrail(moonOrbitR, 0, 0, 0x0d1a2a, 0.08);
scene.add(moonTrail);
const moonBody = new TH.OrbitalBody({
mesh: moonMesh,
orbitRadius: moonOrbitR,
period: moon.period,
});
orbBody.children.push(moonBody);
});
}
// Stations in orbit around planets
if (sys.stations && body.type === 'terrestrial') {
sys.stations.forEach((stn, si) => {
const stnMesh = TH.createOrbitalStation(planetSize * 0.25, 0x22d3ee);
scene.add(stnMesh);
const stnOrbitR = planetSize * 2.5 + si * 1.5;
const stnTrail = TH.createOrbitTrail(stnOrbitR, 0, 0, 0x22d3ee, 0.06);
scene.add(stnTrail);
const stnBody = new TH.OrbitalBody({
mesh: stnMesh,
orbitRadius: stnOrbitR,
period: 3 + si,
phase: si * Math.PI * 0.7,
});
orbBody.children.push(stnBody);
});
}
orbitalBodiesRef.current.push(orbBody);
});
});
// Connection lines
connLinesRef.current = [];
connections.forEach(([a, b]) => {
const sa = systems.find(s => s.id === a);
const sb = systems.find(s => s.id === b);
if (!sa || !sb) return;
const line = TH.createConnectionLine(
{ x: sa.x * SCALE, y: sa.y * SCALE, z: 0 },
{ x: sb.x * SCALE, y: sb.y * SCALE, z: 0 },
0x1c2a3f, 0.4
);
scene.add(line);
connLinesRef.current.push({ line, a, b });
});
// Camera controller
const ctrl = new TH.OrbitController(camera, renderer.domElement, new THREE.Vector3(0, 0, 0));
ctrl.minDistance = 8;
ctrl.maxDistance = 400;
ctrl.dampingFactor = 0.08;
ctrl.rotateSpeed = 0.4;
ctrl.zoomSpeed = 0.8;
ctrl.panSpeed = 0.5;
ctrl.enablePan = true;
ctrl.panButton = 2;
cameraCtrlRef.current = ctrl;
sceneRef.current = { scene, camera, renderer, stars };
// Animation loop
const clock = new THREE.Clock();
const animate = () => {
animIdRef.current = requestAnimationFrame(animate);
const dt = clock.getDelta();
const t = clock.elapsedTime;
ctrl.update(dt);
stars.rotation.y = t * 0.001;
// Pulse system glows
systemMeshesRef.current.forEach(group => {
const glow = group.children[1];
if (glow) {
const base = 6 * SCALE;
glow.scale.setScalar(base + Math.sin(t * 2 + group.position.x) * 0.5);
}
});
// Update orbital bodies (Keplerian physics)
orbitalBodiesRef.current.forEach(ob => ob.update(dt));
// Update asteroid belts
asteroidBeltsRef.current.forEach(ab => TH.updateAsteroidBelt(ab.belt, dt, ab.parentPos));
renderer.render(scene, camera);
};
animate();
const onResize = () => TH.handleResize(renderer, camera, container);
window.addEventListener('resize', onResize);
return () => {
if (animIdRef.current) cancelAnimationFrame(animIdRef.current);
window.removeEventListener('resize', onResize);
if (cameraCtrlRef.current) cameraCtrlRef.current.dispose();
};
}, [systems, celestialData]);
/* ── Draw minimap ── */
useEffect(() => {
const canvas = minimapCanvasRef.current;
if (!canvas || systems.length === 0) return;
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const xs = systems.map(s => s.x);
const ys = systems.map(s => s.y);
const minX = Math.min(...xs) - 40;
const maxX = Math.max(...xs) + 40;
const minY = Math.min(...ys) - 40;
const maxY = Math.max(...ys) + 40;
const scaleX = (W - 20) / (maxX - minX);
const scaleY = (H - 20) / (maxY - minY);
const sc = Math.min(scaleX, scaleY);
const mapX = (x) => 10 + (x - minX) * sc;
const mapY = (y) => H - 10 - (y - minY) * sc;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = 'rgba(8,12,20,0.95)';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(28,42,63,0.6)';
ctx.lineWidth = 0.5;
connections.forEach(([a, b]) => {
const sa = systems.find(s => s.id === a);
const sb = systems.find(s => s.id === b);
if (!sa || !sb) return;
ctx.beginPath(); ctx.moveTo(mapX(sa.x), mapY(sa.y)); ctx.lineTo(mapX(sb.x), mapY(sb.y)); ctx.stroke();
});
if (route.length > 1) {
ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 2]);
ctx.beginPath();
route.forEach((s, i) => { if (i === 0) ctx.moveTo(mapX(s.x), mapY(s.y)); else ctx.lineTo(mapX(s.x), mapY(s.y)); });
ctx.stroke(); ctx.setLineDash([]);
}
systems.forEach(sys => {
const x = mapX(sys.x), y = mapY(sys.y);
const isSel = selected && selected.id === sys.id;
const isDest = destination === sys.id;
const isHov = hovered && hovered.id === sys.id;
let col = '#5a6b82';
if (sys.security >= 0.5) col = '#22c55e';
else if (sys.security >= 0.2) col = '#f0a030';
else col = '#ef4444';
if (isSel) { ctx.fillStyle = '#f0a030'; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); }
if (isDest) { ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.stroke(); }
const r = isHov ? 3 : isSel ? 4 : 2.5;
ctx.fillStyle = col; ctx.globalAlpha = isHov || isSel ? 1 : 0.7;
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1;
if (isSel || isHov || isDest) {
ctx.font = '8px JetBrains Mono, monospace';
ctx.fillStyle = isSel ? '#f1f5f9' : '#94a3b8';
ctx.textAlign = 'center'; ctx.fillText(sys.name, x, y - 8);
}
});
}, [systems, connections, selected, hovered, destination, route]);
/* ── Update route 3D line ── */
useEffect(() => {
if (!sceneRef.current) return;
const { scene } = sceneRef.current;
if (routeLineRef.current) {
scene.remove(routeLineRef.current);
if (routeLineRef.current.geometry) routeLineRef.current.geometry.dispose();
if (routeLineRef.current.material) routeLineRef.current.material.dispose();
routeLineRef.current = null;
}
if (route.length > 1) {
const line = TH.createRouteLine(route.map(s => ({ x: s.x * SCALE, y: s.y * SCALE, z: 1 })), 0x22d3ee);
scene.add(line);
routeLineRef.current = line;
}
}, [route]);
/* ── Update selection visuals ── */
useEffect(() => {
systemMeshesRef.current.forEach(group => {
const sys = group.userData;
const isSel = selected && selected.id === sys.id;
const isHov = hovered && hovered.id === sys.id;
const isDest = destination === sys.id;
const isWP = waypoints.some(w => w.id === sys.id);
const core = group.children[0];
if (core) core.scale.setScalar(isSel ? 2.0 : isHov ? 1.5 : isDest ? 1.6 : isWP ? 1.3 : 1);
const glow = group.children[1];
if (glow) {
let col = new THREE.Color(sys.color);
if (isDest) col = new THREE.Color(0x22d3ee);
else if (isSel) col = new THREE.Color(0xf0a030);
else if (isWP) col = new THREE.Color(0xa78bfa);
glow.material.color.copy(col);
}
const label = group.children[2];
if (label) {
let color = '#5a6b82', size = 18;
if (isSel) { color = '#f1f5f9'; size = 22; }
else if (isHov) { color = '#d4dce8'; size = 20; }
else if (isDest) { color = '#22d3ee'; size = 20; }
else if (isWP) { color = '#a78bfa'; size = 19; }
const prefix = isDest ? '⊕ ' : isWP ? '◇ ' : '';
TH.updateLabelText(label, prefix + sys.name, color, size);
}
});
}, [selected, hovered, destination, waypoints]);
/* ── Highlight connections ── */
useEffect(() => {
connLinesRef.current.forEach(({ line, a, b }) => {
const isOnRoute = route.some((s, i) => {
const next = route[i + 1];
return next && ((a === s.id && b === next.id) || (b === s.id && a === next.id));
});
const isActive = selected && (selected.id === a || selected.id === b);
if (isOnRoute) { line.material.color.set(0x22d3ee); line.material.opacity = 0.5; }
else if (isActive) { line.material.color.set(0xf0a030); line.material.opacity = 0.5; }
else { line.material.color.set(0x1c2a3f); line.material.opacity = 0.25; }
});
}, [selected, route]);
/* ── Raycast helper ── */
const raycastSystems = useCallback((e) => {
if (!sceneRef.current) return null;
const { camera } = sceneRef.current;
const rect = containerRef.current.getBoundingClientRect();
const mouse = new THREE.Vector2(
((e.clientX - rect.left) / rect.width) * 2 - 1,
-((e.clientY - rect.top) / rect.height) * 2 + 1
);
const cores = systemMeshesRef.current.map(g => g.children[0]).filter(Boolean);
const hits = TH.raycast(mouse, camera, cores);
if (hits.length > 0) return hits[0].object.parent;
return null;
}, []);
/* ── Click ── */
const handleClick = useCallback((e) => {
const group = raycastSystems(e);
if (group) setSelected(group.userData);
else setSelected(null);
}, [raycastSystems]);
/* ── Double-click ── */
const handleDoubleClick = useCallback((e) => {
const group = raycastSystems(e);
if (group) {
const sys = group.userData;
setSelected(sys);
focusTargetRef.current = { x: sys.x * SCALE, y: sys.y * SCALE, z: 0 };
}
}, [raycastSystems]);
/* ── Hover ── */
const handleMove = useCallback((e) => {
const group = raycastSystems(e);
if (group) { setHovered(group.userData); containerRef.current.style.cursor = 'pointer'; }
else { setHovered(null); containerRef.current.style.cursor = 'grab'; }
}, [raycastSystems]);
/* ── Right-click context menu ── */
const handleContextMenu = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const group = raycastSystems(e);
setContextMenu({
visible: true,
x: Math.min(e.clientX, window.innerWidth - 200),
y: Math.min(e.clientY, window.innerHeight - 250),
target: group ? group.userData : null,
});
}, [raycastSystems]);
/* ── Actions ── */
const setDestFor = useCallback((sys) => {
setDestination(sys.id);
const sol = systems.find(s => s.id === 'sol');
setRoute(sol && sys.id !== 'sol' ? [sol, sys] : [sys]);
}, [systems]);
const addWaypointFor = useCallback((sys) => {
setWaypoints(prev => {
if (prev.some(w => w.id === sys.id)) return prev;
const next = [...prev, sys];
const sol = systems.find(s => s.id === 'sol');
setRoute(sol ? [sol, ...next] : next);
return next;
});
}, [systems]);
const handleSetDestination = useCallback(() => { if (selected) setDestFor(selected); }, [selected, setDestFor]);
const handleAddWaypoint = useCallback(() => { if (selected) addWaypointFor(selected); }, [selected, addWaypointFor]);
const handleClearRoute = useCallback(() => { setDestination(null); setWaypoints([]); setRoute([]); }, []);
const handleZoomTo = useCallback((level) => { if (cameraCtrlRef.current) cameraCtrlRef.current.distance = level; }, []);
const handleResetView = useCallback(() => {
const ctrl = cameraCtrlRef.current;
if (!ctrl) return;
ctrl.target.set(0, 0, 0); ctrl.distance = 200; ctrl.theta = Math.PI / 4; ctrl.phi = Math.PI / 3;
}, []);
/* ── Filtered systems ── */
const filteredSystems = useMemo(() => {
let list = systems;
if (searchQuery) { const q = searchQuery.toLowerCase(); list = list.filter(s => s.name.toLowerCase().includes(q)); }
if (systemFilter !== 'all') {
if (systemFilter === 'highsec') list = list.filter(s => s.security >= 0.5);
else if (systemFilter === 'lowsec') list = list.filter(s => s.security >= 0.2 && s.security < 0.5);
else if (systemFilter === 'nullsec') list = list.filter(s => s.security < 0.2);
}
return list;
}, [systems, searchQuery, systemFilter]);
const currentSystem = systems.find(s => s.id === 'sol');
const destSystem = destination ? systems.find(s => s.id === destination) : null;
const secColor = (sec) => { if (sec >= 0.5) return 'var(--green)'; if (sec >= 0.2) return 'var(--accent)'; return 'var(--red)'; };
const secBg = (sec) => { if (sec >= 0.5) return 'var(--green-bg)'; if (sec >= 0.2) return 'var(--accent-bg)'; return 'var(--red-bg)'; };
const planetIcon = (t) => { if (t === 'gas') return '⛽'; if (t === 'terrestrial') return '🌍'; return '●'; };
/* ══════════════════════════════════════
RENDER
══════════════════════════════════════ */
return (
<div style={{ position: 'relative', width: '100%', height: '100vh', background: '#040810', overflow: 'hidden' }}>
{/* 3D Canvas */}
<div ref={containerRef} style={{ position: 'absolute', inset: 0, zIndex: 0, cursor: 'grab' }}
onClick={handleClick} onDoubleClick={handleDoubleClick} onMouseMove={handleMove} onContextMenu={handleContextMenu} />
{/* ═══ Custom Context Menu ═══ */}
{contextMenu.visible && (
<div onClick={(e) => e.stopPropagation()} style={{
position: 'fixed', left: contextMenu.x, top: contextMenu.y, zIndex: 9999, minWidth: '200px',
background: 'rgba(10,16,28,0.97)', border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(16px)',
boxShadow: '0 8px 40px rgba(0,0,0,0.7), 0 0 1px rgba(34,211,238,0.15)', overflow: 'hidden',
fontFamily: 'var(--font-mono)', fontSize: '11px',
}}>
{contextMenu.target ? (
<>
{/* System header */}
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(15,22,35,0.6)' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: contextMenu.target.color, boxShadow: `0 0 6px ${contextMenu.target.color}` }} />
<span style={{ flex: 1, color: 'var(--fg-bright)', fontWeight: 600, fontSize: '12px' }}>{contextMenu.target.name}</span>
<span style={{ fontSize: '9px', padding: '1px 5px', borderRadius: 'var(--radius-pill)', background: secBg(contextMenu.target.security), color: secColor(contextMenu.target.security) }}>
{contextMenu.target.security.toFixed(1)}
</span>
</div>
{/* Quick info */}
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: '3px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)', fontSize: '9px' }}>TYPE</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '9px' }}>{contextMenu.target.type}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)', fontSize: '9px' }}>PLANETS</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '9px' }}>{contextMenu.target.planets}</span>
</div>
{celestialData[contextMenu.target.id] && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)', fontSize: '9px' }}>FACTION</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '9px' }}>{celestialData[contextMenu.target.id].faction}</span>
</div>
)}
</div>
{/* Actions */}
<div style={{ padding: '4px' }}>
{[
{ icon: '✦', label: 'Show Details', action: () => { setSelected(contextMenu.target); focusTargetRef.current = { x: contextMenu.target.x * SCALE, y: contextMenu.target.y * SCALE, z: 0 }; setContextMenu(prev => ({...prev, visible: false})); } },
{ icon: '⊕', label: 'Set Destination', action: () => { setDestFor(contextMenu.target); setContextMenu(prev => ({...prev, visible: false})); }, highlight: 'var(--cyan)' },
{ icon: '◇', label: 'Add Waypoint', action: () => { addWaypointFor(contextMenu.target); setContextMenu(prev => ({...prev, visible: false})); } },
{ icon: '◎', label: 'Focus Camera', action: () => { focusTargetRef.current = { x: contextMenu.target.x * SCALE, y: contextMenu.target.y * SCALE, z: 0 }; setContextMenu(prev => ({...prev, visible: false})); } },
{ icon: '⚐', label: 'Bookmark', action: () => setContextMenu(prev => ({...prev, visible: false})) },
].map((item, i) => (
<div key={i} onClick={item.action} style={{
padding: '7px 10px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '10px',
color: 'var(--fg-dim)', borderRadius: 'var(--radius-sm)', transition: 'all 0.1s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-bright)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-dim)'; }}>
<span style={{ width: '16px', textAlign: 'center', color: item.highlight || (i === 0 ? 'var(--cyan)' : 'var(--muted)') }}>{item.icon}</span>
{item.label}
</div>
))}
</div>
</>
) : (
<>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', color: 'var(--muted)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Star Map Controls
</div>
<div style={{ padding: '4px' }}>
{[
{ icon: '⟳', label: 'Reset View', action: () => { handleResetView(); setContextMenu(prev => ({...prev, visible: false})); } },
{ icon: '⊞', label: 'Zoom — Region', action: () => { handleZoomTo(120); setContextMenu(prev => ({...prev, visible: false})); } },
{ icon: '⊡', label: 'Zoom — Show All', action: () => { handleZoomTo(280); setContextMenu(prev => ({...prev, visible: false})); } },
{ icon: '◎', label: 'Zoom — System', action: () => { handleZoomTo(30); setContextMenu(prev => ({...prev, visible: false})); } },
].map((item, i) => (
<div key={i} onClick={item.action} style={{
padding: '7px 10px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '10px',
color: 'var(--fg-dim)', borderRadius: 'var(--radius-sm)', transition: 'all 0.1s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-bright)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-dim)'; }}>
<span style={{ width: '16px', textAlign: 'center', color: 'var(--muted)' }}>{item.icon}</span>
{item.label}
</div>
))}
</div>
</>
)}
</div>
)}
{/* ═══ HUD Overlay ═══ */}
<div style={{ position: 'absolute', inset: 0, zIndex: 1, display: 'flex', flexDirection: 'column', pointerEvents: 'none' }}>
{/* ═══ TOP BAR ═══ */}
<div style={{
height: '40px', display: 'flex', alignItems: 'center', padding: '0 14px', gap: '12px',
background: 'linear-gradient(180deg, rgba(8,12,20,0.94) 0%, rgba(8,12,20,0.6) 80%, transparent 100%)',
fontFamily: 'var(--font-mono)', fontSize: '11px', pointerEvents: 'auto', flexShrink: 0,
}}>
<button onClick={() => window.history.back()} title="Back to Docs" style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', color: 'var(--muted)', cursor: 'pointer', padding: '2px 8px', fontSize: '11px', display: 'flex', alignItems: 'center', gap: '4px', transition: 'all 0.15s' }}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--fg-bright)'; e.currentTarget.style.borderColor = 'var(--border-light)'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
>
<span style={{ fontSize: '9px' }}></span> DOCS
</button>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ fontSize: '12px', fontWeight: 600, color: 'var(--fg-bright)', fontFamily: 'var(--font-display)' }}>STAR MAP</span>
<span style={{ fontSize: '9px', padding: '2px 8px', borderRadius: 'var(--radius-pill)', fontWeight: 600, background: secBg(currentSystem?.security || 1), color: secColor(currentSystem?.security || 1), border: `1px solid ${secColor(currentSystem?.security || 1)}33` }}>
{currentSystem?.name || 'Sol'} · {currentSystem?.security.toFixed(1) || '1.0'}
</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Systems</span>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600 }}>{systems.length}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Bridges</span>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600 }}>{connections.length}</span>
{destination && (
<>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--cyan)', fontSize: '9px', display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 6px var(--cyan)' }} />
DEST: {destSystem?.name}
<span style={{ color: 'var(--muted)' }}>· {route.length - 1} jump{route.length - 1 !== 1 ? 's' : ''}</span>
</span>
</>
)}
<div style={{ marginLeft: 'auto', display: 'flex', gap: '4px', alignItems: 'center' }}>
<span style={{ color: 'var(--muted)', fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Zoom</span>
{[{ label: 'SYS', dist: 30 }, { label: 'REG', dist: 120 }, { label: 'ALL', dist: 280 }].map(z => (
<button key={z.label} onClick={() => handleZoomTo(z.dist)} style={{
padding: '2px 8px', fontSize: '9px', fontFamily: 'var(--font-mono)', borderRadius: 'var(--radius-sm)',
background: 'var(--surface-raised)', border: '1px solid var(--border)', color: 'var(--fg-dim)', cursor: 'pointer', fontWeight: 500,
}}>{z.label}</button>
))}
<div style={{ width: 1, height: 14, background: 'var(--border-light)', margin: '0 4px' }} />
<button onClick={handleResetView} style={{
padding: '2px 8px', fontSize: '9px', fontFamily: 'var(--font-mono)', borderRadius: 'var(--radius-sm)',
background: 'var(--surface-raised)', border: '1px solid var(--border)', color: 'var(--muted)', cursor: 'pointer',
}}>RESET</button>
</div>
</div>
{/* ═══ MIDDLE AREA ═══ */}
<div style={{ flex: 1, display: 'flex', minHeight: 0, position: 'relative' }}>
{/* ═══ LEFT — System Details Panel ═══ */}
<div style={{
width: '260px', display: 'flex', flexDirection: 'column',
pointerEvents: 'auto', flexShrink: 0, minHeight: 0, overflow: 'hidden',
}}>
<div style={{
flex: 1, minHeight: 0, overflowY: 'auto', padding: '8px',
display: 'flex', flexDirection: 'column', gap: '6px',
}}>
{selected ? (
<>
{/* System Header */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden',
}}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: selected.color, boxShadow: `0 0 8px ${selected.color}` }} />
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: '14px', fontWeight: 600, color: 'var(--fg-bright)' }}>{selected.name}</div>
</div>
<span style={{
fontFamily: 'var(--font-mono)', fontSize: '10px', padding: '1px 6px',
borderRadius: 'var(--radius-pill)', fontWeight: 600,
background: secBg(selected.security), color: secColor(selected.security),
border: `1px solid ${secColor(selected.security)}33`,
}}>{selected.security.toFixed(1)}</span>
</div>
<div style={{ padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)', fontSize: '10px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Star Type</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '11px' }}>{selected.type}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)', fontSize: '10px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Planets</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '11px', fontFamily: 'var(--font-mono)' }}>{selected.planets}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)', fontSize: '10px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Stations</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '11px' }}>{selected.stations?.length || 0}</span>
</div>
{selected.stations && selected.stations.length > 0 && (
<div style={{ marginTop: '2px', maxHeight: '80px', overflowY: 'auto' }}>
{selected.stations.map((stn, i) => (
<div key={i} style={{ fontSize: '9px', color: 'var(--cyan)', padding: '2px 0', display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--cyan)' }} />{stn}
</div>
))}
</div>
)}
</div>
</div>
{/* ═══ System Details (Celestial Bodies) ═══ */}
{selectedCelestial && (
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden',
}}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)' }}>
<div style={{ fontSize: '10px', color: 'var(--fg-dim)', lineHeight: 1.5, marginBottom: '6px' }}>
{selectedCelestial.description}
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<span style={{ fontSize: '8px', fontFamily: 'var(--font-mono)', padding: '2px 6px', borderRadius: 'var(--radius-pill)', background: 'var(--accent-bg)', color: 'var(--accent)', border: '1px solid var(--accent-border)' }}>
{selectedCelestial.faction}
</span>
<span style={{ fontSize: '8px', fontFamily: 'var(--font-mono)', padding: '2px 6px', borderRadius: 'var(--radius-pill)', background: 'var(--surface-raised)', color: 'var(--fg-dim)' }}>
Pop: {selectedCelestial.population}
</span>
</div>
</div>
{/* Resources */}
{selectedCelestial.resources.length > 0 && (
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
<span style={{ fontSize: '8px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--muted)', marginRight: '4px' }}>Ore:</span>
{selectedCelestial.resources.map((r, i) => (
<span key={i} style={{ fontSize: '8px', fontFamily: 'var(--font-mono)', padding: '1px 5px', borderRadius: 'var(--radius-pill)', background: 'var(--purple-bg)', color: 'var(--purple)' }}>{r}</span>
))}
</div>
)}
{/* Celestial Bodies List */}
<div style={{ padding: '4px 6px', maxHeight: '220px', overflowY: 'auto' }}>
{selectedCelestial.bodies.map((body, i) => (
<div key={i} style={{
padding: '5px 8px', display: 'flex', alignItems: 'center', gap: '6px',
borderBottom: i < selectedCelestial.bodies.length - 1 ? '1px solid rgba(28,42,63,0.3)' : 'none',
}}>
{/* Icon */}
<span style={{ width: '14px', height: '14px', borderRadius: body.type === 'belt' ? '2px' : '50%', background: body.color || '#5a4a3a', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '6px' }}>
{body.type === 'belt' ? '·' : ''}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: '10px', color: 'var(--fg-dim)', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{body.name}</div>
<div style={{ fontSize: '8px', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
{body.type === 'belt'
? `${body.count} objects · ${body.innerOrbit}-${body.outerOrbit} AU`
: `${body.type} · orbit ${body.orbit} AU · T ${body.period.toFixed(1)}s`
}
</div>
</div>
{body.ecc > 0.05 && (
<span style={{ fontSize: '7px', fontFamily: 'var(--font-mono)', color: 'var(--accent)', padding: '1px 4px', background: 'var(--accent-bg)', borderRadius: 'var(--radius-pill)' }}>
e={body.ecc.toFixed(2)}
</span>
)}
{body.moons && body.moons.length > 0 && (
<span style={{ fontSize: '7px', fontFamily: 'var(--font-mono)', color: 'var(--cyan)', padding: '1px 4px', background: 'var(--cyan-bg)', borderRadius: 'var(--radius-pill)' }}>
{body.moons.length} moon{body.moons.length > 1 ? 's' : ''}
</span>
)}
{body.hasRings && (
<span style={{ fontSize: '7px', fontFamily: 'var(--font-mono)', color: 'var(--purple)', padding: '1px 4px', background: 'var(--purple-bg)', borderRadius: 'var(--radius-pill)' }}>rings</span>
)}
</div>
))}
</div>
</div>
)}
{/* Connections */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden',
}}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>Connections</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', marginLeft: 'auto' }}>
{connections.filter(([a, b]) => a === selected.id || b === selected.id).length} jumps
</span>
</div>
<div style={{ padding: '4px 6px', maxHeight: '200px', overflowY: 'auto' }}>
{connections.filter(([a, b]) => a === selected.id || b === selected.id).map(([a, b]) => {
const neighborId = a === selected.id ? b : a;
const neighbor = systems.find(s => s.id === neighborId);
if (!neighbor) return null;
return (
<div key={neighborId}
onClick={() => { setSelected(neighbor); focusTargetRef.current = { x: neighbor.x * SCALE, y: neighbor.y * SCALE, z: 0 }; }}
onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: neighbor }); }}
style={{
display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 8px',
borderRadius: '4px', cursor: 'pointer', transition: 'background 0.15s',
borderLeft: `2px solid ${secColor(neighbor.security)}`,
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-raised)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
<span style={{ flex: 1, fontSize: '10px', color: 'var(--fg-dim)' }}>{neighbor.name}</span>
<span style={{
fontFamily: 'var(--font-mono)', fontSize: '8px', padding: '1px 4px',
borderRadius: 'var(--radius-pill)', background: secBg(neighbor.security), color: secColor(neighbor.security),
}}>{neighbor.security.toFixed(1)}</span>
</div>
);
})}
</div>
</div>
</>
) : (
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', padding: '16px 12px', textAlign: 'center',
}}>
<div style={{ fontSize: '1.5rem', marginBottom: '8px', color: 'var(--accent)' }}></div>
<div style={{ fontSize: '11px', color: 'var(--fg-dim)', lineHeight: 1.6 }}>
Click a system to select<br />Double-click to focus<br />Right-click for options<br />Scroll to zoom
</div>
</div>
)}
</div>
</div>
{/* CENTER — hover tooltip */}
<div style={{ flex: 1, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
{hovered && hovered.id !== selected?.id && (
<div style={{
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)', backdropFilter: 'blur(8px)',
padding: '6px 12px', fontFamily: 'var(--font-mono)', fontSize: '10px',
display: 'flex', alignItems: 'center', gap: '8px', whiteSpace: 'nowrap',
}}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: hovered.color }} />
<span style={{ color: 'var(--fg-bright)', fontWeight: 600 }}>{hovered.name}</span>
<span style={{ color: secColor(hovered.security), fontSize: '9px' }}>SEC {hovered.security.toFixed(1)}</span>
<span style={{ color: 'var(--muted)', fontSize: '9px' }}>{hovered.type}</span>
{celestialData[hovered.id] && (
<span style={{ color: 'var(--muted)', fontSize: '8px' }}>{celestialData[hovered.id].bodies.length} bodies</span>
)}
</div>
)}
</div>
{/* ═══ RIGHT — System List ═══ */}
<div style={{
width: '220px', display: 'flex', flexDirection: 'column',
pointerEvents: 'auto', flexShrink: 0, minHeight: 0, overflow: 'hidden',
}}>
<div style={{
flex: 1, minHeight: 0, overflowY: 'auto', padding: '8px',
display: 'flex', flexDirection: 'column', gap: '6px',
}}>
{/* Search */}
<div style={{ position: 'relative' }}>
<input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search systems..." style={{
width: '100%', padding: '6px 10px 6px 28px', fontSize: '10px', fontFamily: 'var(--font-mono)',
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
color: 'var(--fg)', backdropFilter: 'blur(8px)', outline: 'none',
}} />
<span style={{ position: 'absolute', left: '10px', top: '50%', transform: 'translateY(-50%)', fontSize: '11px', color: 'var(--muted)' }}></span>
</div>
{/* Filter tabs */}
<div style={{ display: 'flex', borderRadius: 'var(--radius-md)', overflow: 'hidden', border: '1px solid var(--border)', background: 'rgba(15,22,35,0.92)' }}>
{[
{ key: 'all', label: 'ALL' },
{ key: 'highsec', label: 'HI', col: 'var(--green)' },
{ key: 'lowsec', label: 'LO', col: 'var(--accent)' },
{ key: 'nullsec', label: 'NULL', col: 'var(--red)' },
].map(f => (
<button key={f.key} onClick={() => setSystemFilter(f.key)} style={{
flex: 1, padding: '4px 0', fontSize: '8px', fontFamily: 'var(--font-mono)',
textTransform: 'uppercase', letterSpacing: '0.06em', border: 'none',
background: systemFilter === f.key ? 'var(--surface-raised)' : 'transparent',
color: systemFilter === f.key ? (f.col || 'var(--fg-bright)') : 'var(--muted)',
cursor: 'pointer', borderBottom: systemFilter === f.key ? `1.5px solid ${f.col || 'var(--accent)'}` : '1.5px solid transparent',
}}>{f.label}</button>
))}
</div>
{/* System list */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden', flex: 1,
display: 'flex', flexDirection: 'column', minHeight: 0,
}}>
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>Systems</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', marginLeft: 'auto' }}>{filteredSystems.length}</span>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '2px' }}>
{filteredSystems.map(sys => {
const isSel = selected?.id === sys.id;
const isDest = destination === sys.id;
const isWP = waypoints.some(w => w.id === sys.id);
return (
<div key={sys.id}
onClick={() => setSelected(sys)}
onDoubleClick={() => { setSelected(sys); focusTargetRef.current = { x: sys.x * SCALE, y: sys.y * SCALE, z: 0 }; }}
onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: sys }); }}
style={{
display: 'flex', alignItems: 'center', gap: '6px', padding: '5px 8px',
borderRadius: '4px', cursor: 'pointer', transition: 'background 0.15s',
background: isSel ? 'var(--accent-bg)' : isDest ? 'var(--cyan-bg)' : 'transparent',
borderLeft: `2px solid ${isDest ? 'var(--cyan)' : isWP ? 'var(--purple)' : secColor(sys.security)}`,
}}
onMouseEnter={(e) => { if (!isSel && !isDest) e.currentTarget.style.background = 'var(--surface-raised)'; }}
onMouseLeave={(e) => { if (!isSel && !isDest) e.currentTarget.style.background = 'transparent'; }}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', background: sys.color, flexShrink: 0 }} />
<span style={{ flex: 1, fontSize: '10px', color: isSel ? 'var(--fg-bright)' : 'var(--fg-dim)', fontWeight: isSel ? 600 : 400, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{isDest ? '⊕ ' : isWP ? '◇ ' : ''}{sys.name}
</span>
<span style={{
fontFamily: 'var(--font-mono)', fontSize: '8px', padding: '1px 4px',
borderRadius: 'var(--radius-pill)', background: secBg(sys.security), color: secColor(sys.security),
}}>{sys.security.toFixed(1)}</span>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
{/* ═══ BOTTOM BAR ═══ */}
<div style={{ display: 'flex', gap: '6px', padding: '0 8px 8px', pointerEvents: 'auto', flexShrink: 0, alignItems: 'stretch' }}>
{/* Route */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden', flex: 2,
}}>
<div style={{ padding: '5px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--cyan)', display: 'inline-block', marginRight: '4px' }} />Autopilot Route
</span>
{route.length > 1 && (
<>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--cyan)', marginLeft: '4px' }}>{route.length - 1} jump{route.length - 1 !== 1 ? 's' : ''}</span>
<button onClick={handleClearRoute} style={{
marginLeft: 'auto', padding: '1px 6px', fontSize: '8px', fontFamily: 'var(--font-mono)',
background: 'var(--red-bg)', border: '1px solid rgba(239,68,68,0.2)', borderRadius: 'var(--radius-sm)', color: 'var(--red)', cursor: 'pointer',
}}>CLEAR</button>
</>
)}
{!route.length && <span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--muted)', marginLeft: 'auto' }}>No route set</span>}
</div>
{route.length > 0 && (
<div style={{ padding: '6px 10px', display: 'flex', alignItems: 'center', gap: '4px', overflowX: 'auto', scrollbarWidth: 'thin' }}>
{route.map((sys, i) => (
<React.Fragment key={sys.id}>
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px',
padding: '2px 8px', borderRadius: '4px',
background: i === route.length - 1 ? 'var(--cyan-bg)' : i === 0 ? 'var(--green-bg)' : 'transparent', minWidth: '60px',
}}>
<span style={{ fontSize: '9px', color: i === 0 ? 'var(--green)' : i === route.length - 1 ? 'var(--cyan)' : 'var(--fg-dim)', fontWeight: 600 }}>
{i === 0 ? '⬡' : i === route.length - 1 ? '⊕' : '◇'} {sys.name}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '7px', color: secColor(sys.security) }}>{sys.security.toFixed(1)}</span>
</div>
{i < route.length - 1 && <span style={{ color: 'var(--cyan)', fontSize: '10px', opacity: 0.4 }}></span>}
</React.Fragment>
))}
</div>
)}
</div>
{/* Ship Status */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden', flex: 0.8,
}}>
<div style={{ padding: '5px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)', display: 'inline-block', marginRight: '4px' }} />Ship
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', marginLeft: 'auto' }}>Venture</span>
</div>
<div style={{ padding: '6px 10px', display: 'flex', gap: '12px', alignItems: 'center' }}>
{[{ label: 'SH', value: 100, color: '#22d3ee' }, { label: 'AR', value: 100, color: '#f0a030' }, { label: 'HU', value: 100, color: '#22c55e' }, { label: 'CA', value: 85, color: '#a78bfa' }].map(bar => (
<div key={bar.label} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '3px', flex: 1 }}>
<div style={{ width: '100%', height: '3px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: bar.color, borderRadius: 'var(--radius-pill)' }} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '7px', color: bar.color }}>{bar.label}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '7px', color: 'var(--fg-dim)' }}>{bar.value}%</span>
</div>
</div>
))}
</div>
</div>
{/* Wallet & Time */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden', flex: 0.5,
display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '6px 10px',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '13px', fontWeight: 700, color: 'var(--accent)', letterSpacing: '-0.02em' }}>125,000</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: '3px' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 4px var(--green)' }} />
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)' }}>Online</span>
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--muted)', marginLeft: 'auto' }}>14:23 UTC</span>
</div>
</div>
{/* Minimap */}
<div style={{
background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(10px)', overflow: 'hidden', width: '180px', flexShrink: 0,
}}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>Overview</span>
</div>
<div style={{ padding: '4px' }}>
<canvas ref={minimapCanvasRef} width={168} height={110} style={{ width: '100%', height: 'auto', borderRadius: 'var(--radius-sm)', display: 'block' }} />
</div>
</div>
</div>
</div>
</div>
);
}
window.GDD.StarMapDemo = StarMapDemo;