diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 793e371..3b0ea4e 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo, useState, ReactElement, useContext } from 'react'; +import React, { useMemo, useState, ReactElement, useContext, useRef, useEffect, memo } from 'react'; import { Mosaic, MosaicWindow, @@ -29,6 +29,132 @@ type MosaicKey = | 'goalSetter' | 'networkHealthMonitor'; +// Human-readable tile names mapping +const TILE_DISPLAY_NAMES: Record = { + mapView: 'Map View', + rosMonitor: 'System Telemetry', + waypointList: 'Waypoint List', + videoControls: 'Video Stream', + gasSensor: 'Science', + orientationDisplay: 'Rover Orientation', + goalSetter: 'Nav2', + networkHealthMonitor: 'Connection Health', +}; + +// All available tiles - no need for useMemo as this is a static array +const ALL_TILES: MosaicKey[] = [ + 'mapView', + 'rosMonitor', + 'networkHealthMonitor', + 'orientationDisplay', + 'videoControls', + 'waypointList', + 'gasSensor', + 'goalSetter', +]; + +type PendingAdd = { + pathKey: string; + path: MosaicPath; + direction: 'row' | 'column'; +} | null; + +// Move Controls component outside to prevent re-creation on every render +const Controls = memo<{ + id: MosaicKey; + path: MosaicPath; + pendingAdd: PendingAdd; + setPendingAdd: (value: PendingAdd) => void; +}>(({ id, path, pendingAdd, setPendingAdd }) => { + const { mosaicActions } = useContext(MosaicContext); + const pathKey = JSON.stringify(path); + const showDropdown = pendingAdd?.pathKey === pathKey; + const dropdownRef = useRef(null); + + const splitAndAdd = (direction: 'row' | 'column', newTile: MosaicKey) => { + const splitNode: MosaicNode = { + direction, + first: id, // keep current tile + second: newTile, + splitPercentage: 60, + }; + + mosaicActions.replaceWith(path, splitNode); + setPendingAdd(null); + }; + + // Close dropdown when clicking outside + useEffect(() => { + if (!showDropdown) return; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setPendingAdd(null); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDropdown, setPendingAdd]); + + return ( +
+ + + + + {showDropdown ? ( + + ) : null} +
+ ); +}); + +Controls.displayName = 'Controls'; + const MosaicDashboard: React.FC = () => { // TODO: parameterize layout for custom layout configs const [mosaicLayout, setMosaicLayout] = useState | null>({ @@ -66,91 +192,8 @@ const MosaicDashboard: React.FC = () => { splitPercentage: 60, }); - const ALL_TILES = useMemo( - () => [ - 'mapView', - 'rosMonitor', - 'networkHealthMonitor', - 'orientationDisplay', - 'videoControls', - 'waypointList', - 'gasSensor', - 'goalSetter', - ], - [] - ); - // Which window currently has the dropdown open? - const [pendingAdd, setPendingAdd] = useState<{ - pathKey: string; - path: MosaicPath; - direction: 'row' | 'column'; // row => add right, column => add below - } | null>(null); - - const Controls: React.FC<{ id: MosaicKey; path: MosaicPath }> = ({ id, path }) => { - const { mosaicActions } = useContext(MosaicContext); - const pathKey = JSON.stringify(path); - const showDropdown = pendingAdd?.pathKey === pathKey; - - const splitAndAdd = (direction: 'row' | 'column', newTile: MosaicKey) => { - const splitNode: MosaicNode = { - direction, - first: id, // keep current tile - second: newTile, - splitPercentage: 60, - }; - - mosaicActions.replaceWith(path, splitNode); - setPendingAdd(null); - }; - - return ( -
- - - - - {showDropdown ? ( - - ) : null} -
- ); - }; + const [pendingAdd, setPendingAdd] = useState(null); const renderTile = (id: MosaicKey, path: MosaicPath): ReactElement => { switch (id) { @@ -159,7 +202,7 @@ const MosaicDashboard: React.FC = () => { title="Map View" path={path} - additionalControls={} + additionalControls={} >
@@ -172,7 +215,7 @@ const MosaicDashboard: React.FC = () => { title="Waypoint List" path={path} - additionalControls={} + additionalControls={} > @@ -183,7 +226,7 @@ const MosaicDashboard: React.FC = () => { title="Video Stream" path={path} - additionalControls={} + additionalControls={} > @@ -194,7 +237,7 @@ const MosaicDashboard: React.FC = () => { title="System Telemetry" path={path} - additionalControls={} + additionalControls={} > @@ -205,7 +248,7 @@ const MosaicDashboard: React.FC = () => { title="Connection Health" path={path} - additionalControls={} + additionalControls={} > @@ -216,7 +259,7 @@ const MosaicDashboard: React.FC = () => { title="Rover Orientation" path={path} - additionalControls={} + additionalControls={} > @@ -227,7 +270,7 @@ const MosaicDashboard: React.FC = () => { title="Science" path={path} - additionalControls={} + additionalControls={} > @@ -238,7 +281,7 @@ const MosaicDashboard: React.FC = () => { title="Nav2" path={path} - additionalControls={} + additionalControls={} > @@ -249,7 +292,7 @@ const MosaicDashboard: React.FC = () => { title="Unknown tile" path={path} - additionalControls={} + additionalControls={} >
Unknown tile
@@ -292,7 +335,7 @@ const MosaicDashboard: React.FC = () => { .tile-btn { background: transparent; border: 1px solid #444; - color: #2d2d2d; + color: #f1f1f1; border-radius: 6px; padding: 2px 6px; cursor: pointer;