diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx
index 2571027..d04c65b 100644
--- a/src/components/panels/MosaicDashboard.tsx
+++ b/src/components/panels/MosaicDashboard.tsx
@@ -36,60 +36,82 @@ import { ROVER_IP } from '@/constants';
interface TileDefinition {
title: string;
+ category: TileCategory;
render: () => ReactElement;
}
const TILE_REGISTRY = {
mapView: {
title: 'Map View',
+ category: 'navigation',
render: () => (
),
},
- rosMonitor: { title: 'System Telemetry', render: () => },
- networkHealthMonitor: { title: 'Connection Health', render: () => },
- orientationDisplay: { title: 'Rover Orientation', render: () => },
- videoControls: { title: 'Video Stream', render: () => },
- rtpStats: { title: 'RTP Statistics', render: () => },
- driveThrottlePanel: { title: 'Drive Throttle', render: () => },
- waypointList: { title: 'Waypoint List', render: () => },
- goalSetter: { title: 'Nav2', render: () => },
- motorStatusPanel: { title: 'Motor Status', render: () => },
- nodeStatusPanel: { title: 'Node Status', render: () => },
- antennaControlPanel: { title: 'Antenna Control', render: () => },
- scienceControlPanel: { title: 'Science Motor Control', render: () => },
- scienceSensorPanel: { title: 'Science Sensor Readouts', render: () => },
- pdbRails: { title: 'PDB Rails', render: () => },
- armControlPanel: { title: 'Arm Control', render: () => },
+ rosMonitor: { title: 'System Telemetry', category: 'system', render: () => },
+ networkHealthMonitor: { title: 'Connection Health', category: 'comms', render: () => },
+ orientationDisplay: { title: 'Rover Orientation', category: 'armScience', render: () => },
+ videoControls: { title: 'Video Stream', category: 'video', render: () => },
+ rtpStats: { title: 'RTP Statistics', category: 'video', render: () => },
+ driveThrottlePanel: { title: 'Drive Throttle', category: 'drive', render: () => },
+ waypointList: { title: 'Waypoint List', category: 'navigation', render: () => },
+ goalSetter: { title: 'Nav2', category: 'navigation', render: () => },
+ motorStatusPanel: { title: 'Motor Status', category: 'drive', render: () => },
+ nodeStatusPanel: { title: 'Node Status', category: 'system', render: () => },
+ antennaControlPanel: { title: 'Antenna Control', category: 'comms', render: () => },
+ scienceControlPanel: { title: 'Science Motor Control', category: 'armScience', render: () => },
+ scienceSensorPanel: { title: 'Science Sensor Readouts', category: 'armScience', render: () => },
+ pdbRails: { title: 'PDB Rails', category: 'system', render: () => },
+ armControlPanel: { title: 'Arm Control', category: 'armScience', render: () => },
webRTCClient: {
title: 'WebRTC Client',
+ category: 'video',
render: () => ,
},
- timerPanel: { title: 'Multi-Timer', render: () => },
- headlightControlPanel: { title: 'Headlights', render: () => },
- morseTransmissionPanel: { title: 'Morse Transmission', render: () => },
- echoPanel: { title: 'Topic Echo', render: () => },
- motorCurrentGraphPanel: { title: 'Motor Current', render: () => },
+ timerPanel: { title: 'Multi-Timer', category: 'system', render: () => },
+ headlightControlPanel: { title: 'Headlights', category: 'drive', render: () => },
+ morseTransmissionPanel: { title: 'Morse Transmission', category: 'armScience', render: () => },
+ echoPanel: { title: 'Topic Echo', category: 'system', render: () => },
+ motorCurrentGraphPanel: { title: 'Motor Current', category: 'drive', render: () => },
} satisfies Record;
+const TILE_CATEGORIES = {
+ navigation: { label: 'Navigation', color: '#4fc3f7' }, // light blue
+ drive: { label: 'Drive', color: '#ffb74d' }, // orange
+ video: { label: 'Video', color: '#ce93d8' }, // purple
+ comms: { label: 'Comms', color: '#81c784' }, // green
+ armScience: { label: 'Arm & Science', color: '#f48fb1' }, // pink
+ system: { label: 'System & Debug', color: '#b0bec5' }, // grey
+};
+
+type TileCategory = keyof typeof TILE_CATEGORIES;
type TileType = keyof typeof TILE_REGISTRY;
type TileId = `${TileType}:${number}`;
// Object.keys preserves insertion order for string keys
const ALL_TILE_TYPES = Object.keys(TILE_REGISTRY) as TileType[];
+const TILE_GROUPS = (Object.keys(TILE_CATEGORIES) as TileCategory[])
+ .map((category) => ({
+ category,
+ types: ALL_TILE_TYPES
+ .filter((t) => TILE_REGISTRY[t].category === category)
+ .sort((a, b) => TILE_REGISTRY[a].title.localeCompare(TILE_REGISTRY[b].title)),
+ }))
+ .filter((g) => g.types.length > 0)
+
function tileTypeOf(id: TileId): TileType {
return id.split(':', 1)[0] as TileType;
}
type PendingAdd =
| {
- pathKey: string;
- path: MosaicPath;
- direction: 'row' | 'column';
- }
+ pathKey: string;
+ path: MosaicPath;
+ direction: 'row' | 'column';
+ }
| null;
function collectMaxTileNumber(node: MosaicNode | null): number {
@@ -248,11 +270,18 @@ const Controls = memo<{
- {ALL_TILE_TYPES.map((t) => (
-
- ))}
+ {TILE_GROUPS.map(({ category, types }) => {
+ const { label, color } = TILE_CATEGORIES[category];
+ return (
+
+ );
+ })}
) : null}