From 43d8563b79a29ad545feee7cb7e1bb0bdb198122 Mon Sep 17 00:00:00 2001 From: wuxu Date: Sun, 24 May 2026 19:47:05 +0800 Subject: [PATCH] update visual design system --- src/About.js | 115 +++++++++------- src/App.js | 107 +++++++------- src/CompactSidebar.js | 96 ++++++------- src/ConnectionIndicators.js | 34 ++--- src/NodeList.js | 49 +++---- src/NodeLogs.js | 36 +++-- src/NodeProperties.js | 18 +-- src/PanelsMenu.js | 12 +- src/ToolsMenu.js | 14 +- src/css/index.css | 3 +- src/theme.js | 268 +++++++++++++++++++++++++++--------- 11 files changed, 452 insertions(+), 300 deletions(-) diff --git a/src/About.js b/src/About.js index a879aae..8371e77 100644 --- a/src/About.js +++ b/src/About.js @@ -2,35 +2,51 @@ import React from 'react'; import { Typography, Box, Paper, Divider, Link, Grid, Card, CardContent, List, ListItem, ListItemIcon, ListItemText, Stack, Chip, Button } from '@mui/material'; import packageInfo from '../package.json'; import vimdronesLogo from './image/vimdrones_logo.png'; -import discordLogo from './image/discord_logo.png'; // Import Discord logo +import discordLogo from './image/discord_logo.png'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import UsbIcon from '@mui/icons-material/Usb'; import WifiIcon from '@mui/icons-material/Wifi'; import ComputerIcon from '@mui/icons-material/Computer'; -// Remove the DiscordIcon import since we're now using a custom image - const About = () => { return ( - - - - + + + + DroneCAN Web Tools - + + A browser-based console for discovery, parameters, firmware, monitoring, and panel workflows. + - + - Key Features + Key Features @@ -59,22 +75,22 @@ const About = () => { - + - + WebSocket via MAVProxy - + mavproxy.py --master /dev/tty.usbmodem111401,115200 --out wsserver:127.0.0.1:5555 - Connect to ws://127.0.0.1:5555 in the Adapter Settings + Connect to ws://127.0.0.1:5555 in the Adapter Settings - - + + @@ -87,8 +103,8 @@ const About = () => { - - + + @@ -109,23 +125,23 @@ const About = () => { - {/* Changed from info to success */} + - - MAVCAN Bridge - from Vimdrones @@ -138,22 +154,21 @@ const About = () => { - - {/* Footer */} - + + - Author: Huibean Luo - + - + Sponsored by - Vimdrones @@ -271,4 +286,4 @@ const About = () => { ); } -export default About; \ No newline at end of file +export default About; diff --git a/src/App.js b/src/App.js index 0b009cc..9a049be 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { AppBar, Toolbar, Typography, Box, Button, ThemeProvider, IconButton, Snackbar, Alert, Tooltip, FormControl, Select, MenuItem, InputLabel, Chip } from '@mui/material'; +import { AppBar, Toolbar, Typography, Box, Button, ThemeProvider, IconButton, Snackbar, Alert, Tooltip, FormControl, Select, MenuItem } from '@mui/material'; import MavlinkSession from './mavlink_session'; import dronecan from './dronecan'; import theme from './theme'; @@ -97,7 +97,7 @@ const AppContent = () => { const newBus = event.target.value; if (newBus === selectedBus) return; setSelectedBus(newBus); - + if (window.localNode) { window.localNode.changeBus(newBus); showMessage(t('app.bus_switched', { bus: newBus }), 'info'); @@ -140,40 +140,43 @@ const AppContent = () => { } }; + const surfaceBorder = '1px solid rgba(230, 223, 216, 0.95)'; + return ( <> - - - - - + + + + + - - - - DroneCAN + + + + DroneCAN - + {t('app.title')} - - + - - { - - + setLanguage(language === 'en' ? 'zh' : 'en')} - sx={{ mr: 0.5 }} + sx={{ mr: 0.5, border: '1px solid rgba(230, 223, 216, 0.95)', backgroundColor: '#faf9f5' }} > @@ -244,47 +247,47 @@ const AppContent = () => { - - + - - - + - - {selectedNodeId && ( - + { open={modalOpen} onClose={handleCloseModal} onConnectionStatusChange={handleConnectionStatusChange} - showMessage={showMessage.bind(this)} + showMessage={showMessage} selectedBus={selectedBus} onBusChange={handleBusChange} /> setSnackbarOpen(false)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > - setSnackbarOpen(false)} severity={snackbarSeverity}> + setSnackbarOpen(false)} severity={snackbarSeverity} variant="filled"> {snackbarMessage} @@ -322,4 +325,4 @@ const AppContent = () => { ); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/CompactSidebar.js b/src/CompactSidebar.js index c37e41f..311e6a3 100644 --- a/src/CompactSidebar.js +++ b/src/CompactSidebar.js @@ -2,26 +2,22 @@ import React, { useEffect, useState } from 'react'; import { Box, Badge, Tooltip, CircularProgress, Typography } from '@mui/material'; import NotificationsIcon from '@mui/icons-material/Notifications'; -const CompactSidebar = ({ - nodes, - selectedNodeId, - setSelectedNodeId +const CompactSidebar = ({ + nodes, + selectedNodeId, + setSelectedNodeId }) => { const [logCounts, setLogCounts] = useState({}); - + useEffect(() => { - // Function to get log counts from the window.localNode events const updateLogCounts = () => { const counts = {}; - - // Initialize counts for all nodes to 0 + Object.keys(nodes).forEach(nodeId => { counts[nodeId] = 0; }); - + try { - // Access logs if they're stored somewhere else in the app - // Option 1: If logs are stored in a global state/context if (window.dronecanLogs && Array.isArray(window.dronecanLogs)) { window.dronecanLogs.forEach(log => { if (log.id) { @@ -29,63 +25,54 @@ const CompactSidebar = ({ } }); } - // Option 2: Listen for log events and maintain our own count - // This is already set up in the useEffect for event listening below } catch (error) { console.error("Error accessing logs:", error); } - + setLogCounts(counts); }; - - // Create log listener and counter + let lastLogCounts = {}; - + const handleLog = (transfer) => { const sourceNodeId = transfer.sourceNodeId; if (sourceNodeId) { - // Update count for this node lastLogCounts[sourceNodeId] = (lastLogCounts[sourceNodeId] || 0) + 1; setLogCounts({...lastLogCounts}); } }; - - // Listen for log messages + const localNode = window.localNode; if (localNode) { localNode.on('uavcan.protocol.debug.LogMessage', handleLog); } - - // Initial update + updateLogCounts(); - + return () => { - // Remove event listener on cleanup if (localNode) { localNode.off('uavcan.protocol.debug.LogMessage', handleLog); } }; }, [nodes]); - - // Get color based on node mode (matching the same logic as NodeList) + const getModeColor = (mode) => { switch (mode) { case 'OPERATIONAL': - return '#f5f5f5'; // Light gray for operational (subtle) + return '#5db872'; case 'INITIALIZATION': - return '#ffb74d'; // Warning color + return '#d4a017'; case 'MAINTENANCE': - return '#9c27b0'; // Secondary/purple + return '#5db8a6'; case 'SOFTWARE_UPDATE': - return '#4caf50'; // Success/green + return '#5db872'; case 'OFFLINE': - return '#f44336'; // Error/red + return '#c64545'; default: - return '#f44336'; // Default to error color + return '#c64545'; } }; - - // Handle click on a node + const handleNodeClick = (nodeId) => { if (nodeId === selectedNodeId) { setSelectedNodeId(null); @@ -93,10 +80,10 @@ const CompactSidebar = ({ setSelectedNodeId(Number(nodeId)); } }; - + return ( - - NODES @@ -130,11 +119,12 @@ const CompactSidebar = ({ const node = nodes[nodeId]; const mode = node.status.getConstant('mode'); const logCount = logCounts[nodeId] || 0; - + const selected = selectedNodeId === Number(nodeId); + return ( - handleNodeClick(Number(nodeId))} > - {/* Make NID larger and more prominent */} - {nodeId} - + {logCount > 0 && ( 99 ? '99+' : logCount} @@ -190,4 +180,4 @@ const CompactSidebar = ({ ); }; -export default CompactSidebar; \ No newline at end of file +export default CompactSidebar; diff --git a/src/ConnectionIndicators.js b/src/ConnectionIndicators.js index 83780ec..f712adc 100644 --- a/src/ConnectionIndicators.js +++ b/src/ConnectionIndicators.js @@ -7,29 +7,29 @@ const ConnectionIndicators = ({ isConnected, mavlinkSession, localNode }) => { const [rxActive, setRxActive] = useState(false); const txTimeout = useRef(null); const rxTimeout = useRef(null); - + useEffect(() => { const handleFrameSend = () => { setTxActive(true); clearTimeout(txTimeout.current); txTimeout.current = setTimeout(() => { setTxActive(false); - }, 100); // Blink for 200ms + }, 100); }; - + const handleFrameReceive = () => { setRxActive(true); clearTimeout(rxTimeout.current); rxTimeout.current = setTimeout(() => { setRxActive(false); - }, 100); // Blink for 200ms + }, 100); }; - + if (mavlinkSession) { mavlinkSession.on('mav-tx', handleFrameSend); mavlinkSession.on('mav-rx', handleFrameReceive); } - + return () => { if (mavlinkSession) { mavlinkSession.removeListener('mav-tx', handleFrameSend); @@ -39,14 +39,14 @@ const ConnectionIndicators = ({ isConnected, mavlinkSession, localNode }) => { clearTimeout(rxTimeout.current); }; }, [localNode, mavlinkSession]); - + return ( - + - { /> TX - + - { ); }; -export default ConnectionIndicators; \ No newline at end of file +export default ConnectionIndicators; diff --git a/src/NodeList.js b/src/NodeList.js index e23ce51..1c770ca 100644 --- a/src/NodeList.js +++ b/src/NodeList.js @@ -29,16 +29,27 @@ const NodeList = ({ nodes, selectedNodeId, setSelectedNodeId }) => { }; const renderNodeRow = (key) => { - let node = nodes[key]; - let status = node.status; - let health = status.getConstant('health'); - let mode = status.getConstant('mode'); + const node = nodes[key]; + const status = node.status; + const health = status.getConstant('health'); + const mode = status.getConstant('mode'); + const isSelected = Number(key) === Number(selectedNodeId); return ( - handleRowClick((Number(key)))} style={{ cursor: 'pointer' }}> + handleRowClick(Number(key))} + sx={{ + cursor: 'pointer', + backgroundColor: isSelected ? 'rgba(204, 120, 92, 0.08)' : 'transparent', + '&:hover': { + backgroundColor: 'rgba(245, 240, 232, 0.9)', + }, + }} + > {key} - {node.name} + {node.name} {health} - {mode} + {mode} {secondsToTime(status.uptime_sec)} {node.status.vendor_specific_status_code} @@ -48,25 +59,17 @@ const NodeList = ({ nodes, selectedNodeId, setSelectedNodeId }) => { return ( - - Online Nodes + + Online Nodes - + NID - - Name - + Name Health Mode Uptime @@ -74,9 +77,7 @@ const NodeList = ({ nodes, selectedNodeId, setSelectedNodeId }) => { - {Object.keys(nodes).map((key) => ( - renderNodeRow(key) - ))} + {Object.keys(nodes).map((key) => renderNodeRow(key))}
@@ -84,4 +85,4 @@ const NodeList = ({ nodes, selectedNodeId, setSelectedNodeId }) => { ); }; -export default NodeList; \ No newline at end of file +export default NodeList; diff --git a/src/NodeLogs.js b/src/NodeLogs.js index c8b0069..e11346f 100644 --- a/src/NodeLogs.js +++ b/src/NodeLogs.js @@ -16,7 +16,6 @@ const NodeLogs = () => { if (paused) { return; } - // console.log(transfer); const msg = transfer.payload; const msgObj = msg.toObj(); setLogs((logs) => [...logs, { @@ -24,7 +23,7 @@ const NodeLogs = () => { localTime: new Date().toLocaleTimeString(), level: msgObj.level.getConstant('value'), source: '', - text: msgObj.text + text: msgObj.text }]); }; localNode.on('uavcan.protocol.debug.LogMessage', handleLog); @@ -51,21 +50,22 @@ const NodeLogs = () => { return ( - {t('logs.title')} + {t('logs.title')} setPaused(!paused)} @@ -73,22 +73,20 @@ const NodeLogs = () => { {paused ? : } setLogs([])} > - + @@ -101,10 +99,10 @@ const NodeLogs = () => { {logs.map((log, index) => ( - + {log.id} {log.localTime} - + {log.level} {log.source} @@ -120,4 +118,4 @@ const NodeLogs = () => { ); }; -export default NodeLogs; \ No newline at end of file +export default NodeLogs; diff --git a/src/NodeProperties.js b/src/NodeProperties.js index e46bd64..30ddd1a 100644 --- a/src/NodeProperties.js +++ b/src/NodeProperties.js @@ -67,16 +67,16 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit return ( - + {t('props.title')} - {t('props.multi_editor')} + {t('props.multi_editor')} { setMultiNodeEditorEnable(e.target.checked) }} /> @@ -199,7 +199,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit > {t('props.controls')} - + @@ -70,4 +74,4 @@ const PanelsMenu = ({openWindow}) => { ); } -export default PanelsMenu; \ No newline at end of file +export default PanelsMenu; diff --git a/src/ToolsMenu.js b/src/ToolsMenu.js index bdc1b3d..156e596 100644 --- a/src/ToolsMenu.js +++ b/src/ToolsMenu.js @@ -1,8 +1,6 @@ import React, { useState } from 'react'; -import { Box, Button, Menu, MenuItem, Divider } from '@mui/material'; +import { Box, Button, Menu, MenuItem } from '@mui/material'; import BuildIcon from '@mui/icons-material/Build'; -import MessageIcon from '@mui/icons-material/Message'; -import SettingsInputCompositeIcon from '@mui/icons-material/SettingsInputComposite'; import { useTranslation } from './i18n/LanguageContext'; const ToolsMenu =({openWindow}) => { @@ -43,8 +41,12 @@ const ToolsMenu =({openWindow}) => { aria-expanded={open ? 'true' : undefined} disableElevation onClick={handleClick} - color="default" - startIcon={} + color="inherit" + startIcon={} + sx={{ + border: '1px solid rgba(230, 223, 216, 0.95)', + backgroundColor: 'background.default', + }} > {t('tools.title')} @@ -73,4 +75,4 @@ const ToolsMenu =({openWindow}) => { ); } -export default ToolsMenu; \ No newline at end of file +export default ToolsMenu; diff --git a/src/css/index.css b/src/css/index.css index 1043e39..0e0bd9e 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -5,8 +5,9 @@ } body { - background-color: rgb(22, 13, 13); + background-color: #faf9f5; overflow: hidden; + color: #141413; } #root { diff --git a/src/theme.js b/src/theme.js index 6d750bd..92a70f4 100644 --- a/src/theme.js +++ b/src/theme.js @@ -1,49 +1,164 @@ import { createTheme } from '@mui/material/styles'; +const canvas = '#faf9f5'; +const surfaceSoft = '#f5f0e8'; +const surfaceCard = '#efe9de'; +const surfaceCreamStrong = '#e8e0d2'; +const surfaceDark = '#181715'; +const surfaceDarkElevated = '#252320'; +const surfaceDarkSoft = '#1f1e1b'; +const hairline = '#e6dfd8'; +const hairlineSoft = '#ebe6df'; +const ink = '#141413'; +const body = '#3d3d3a'; +const muted = '#6c6a64'; +const mutedSoft = '#8e8b82'; +const primary = '#cc785c'; +const primaryActive = '#a9583e'; +const primaryDisabled = '#e6dfd8'; +const onPrimary = '#ffffff'; +const success = '#5db872'; +const warning = '#d4a017'; +const error = '#c64545'; +const accentTeal = '#5db8a6'; + const theme = createTheme({ palette: { - mode: 'dark', - text: { - primary: '#ffffff', // Set the primary text color to white + mode: 'light', + primary: { + main: primary, + dark: primaryActive, + light: '#d89278', + contrastText: onPrimary, }, + secondary: { + main: accentTeal, + }, + success: { + main: success, + }, + warning: { + main: warning, + }, + error: { + main: error, + }, + background: { + default: canvas, + paper: surfaceCard, + }, + text: { + primary: ink, + secondary: body, + disabled: mutedSoft, + }, + divider: hairline, + }, + shape: { + borderRadius: 12, }, - // Add typography settings to make all fonts smaller typography: { - fontSize: 12, // Default font size in px (reduced from the default 14px) - htmlFontSize: 14, // Base HTML font-size (was 16px by default) - // Customize specific variants + fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontSize: 13, + htmlFontSize: 14, h1: { - fontSize: '2rem', // 24px + fontFamily: 'Cormorant Garamond, Georgia, serif', + fontSize: '2rem', + fontWeight: 400, + letterSpacing: '-0.04em', }, h2: { - fontSize: '1.75rem', // 21px + fontFamily: 'Cormorant Garamond, Georgia, serif', + fontSize: '1.75rem', + fontWeight: 400, + letterSpacing: '-0.03em', }, h3: { - fontSize: '1.5rem', // 18px + fontFamily: 'Cormorant Garamond, Georgia, serif', + fontSize: '1.5rem', + fontWeight: 400, + letterSpacing: '-0.02em', }, h4: { - fontSize: '1.25rem', // 15px + fontFamily: 'Cormorant Garamond, Georgia, serif', + fontSize: '1.25rem', + fontWeight: 400, + letterSpacing: '-0.02em', }, h5: { - fontSize: '1.1rem', // 13.2px + fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontSize: '1.05rem', + fontWeight: 500, }, h6: { - fontSize: '1rem', // 12px + fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontSize: '0.95rem', + fontWeight: 500, }, body1: { - fontSize: '0.875rem', // 10.5px + fontSize: '0.9rem', }, body2: { - fontSize: '0.825rem', // 9.9px + fontSize: '0.82rem', }, button: { - fontSize: '0.825rem', // 9.9px + fontSize: '0.82rem', + fontWeight: 500, + textTransform: 'none', }, caption: { - fontSize: '0.75rem', // 9px + fontSize: '0.75rem', }, }, components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: canvas, + color: ink, + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: canvas, + color: ink, + backgroundImage: 'none', + borderBottom: `1px solid ${hairline}`, + boxShadow: 'none', + }, + }, + }, + MuiToolbar: { + styleOverrides: { + root: { + minHeight: 64, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + backgroundColor: surfaceCard, + color: ink, + border: `1px solid ${hairline}`, + }, + elevation1: { + boxShadow: '0 1px 3px rgba(20, 20, 19, 0.08)', + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + backgroundImage: 'none', + backgroundColor: surfaceCard, + border: `1px solid ${hairline}`, + }, + }, + }, MuiButton: { defaultProps: { size: 'small', @@ -51,19 +166,55 @@ const theme = createTheme({ styleOverrides: { root: { textTransform: 'none', + borderRadius: 8, + boxShadow: 'none', + }, + containedPrimary: { + backgroundColor: primary, + color: onPrimary, + '&:hover': { + backgroundColor: primaryActive, + boxShadow: 'none', + }, + }, + outlined: { + borderColor: hairline, + color: ink, + backgroundColor: canvas, + '&:hover': { + backgroundColor: surfaceSoft, + borderColor: hairline, + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: ink, }, }, }, - // Add table cell specific overrides MuiTableCell: { styleOverrides: { root: { fontSize: '0.8rem', - padding: '4px 8px', + padding: '6px 10px', + borderBottom: `1px solid ${hairlineSoft}`, }, head: { - fontWeight: 'bold', - fontSize: '0.8rem', + fontWeight: 600, + color: muted, + backgroundColor: surfaceSoft, + }, + }, + }, + MuiTableRow: { + styleOverrides: { + root: { + '&:hover': { + backgroundColor: surfaceSoft, + }, }, }, }, @@ -74,25 +225,19 @@ const theme = createTheme({ styleOverrides: { root: { '& .MuiInputBase-root': { - fontSize: '0.8rem', + fontSize: '0.82rem', + backgroundColor: canvas, }, '& .MuiInputLabel-root': { fontSize: '0.8rem', - transform: 'translate(14px, 9px) scale(1)', - }, - '& .MuiInputLabel-shrink': { - transform: 'translate(14px, -6px) scale(0.75)', }, '& .MuiOutlinedInput-root': { - padding: '4px 8px', - }, - '& .MuiOutlinedInput-input': { - padding: '4px', + borderRadius: 8, }, }, }, }, - MuiTable: { + MuiFormControl: { defaultProps: { size: 'small', }, @@ -102,63 +247,56 @@ const theme = createTheme({ size: 'small', }, }, - MuiFormControl: { - defaultProps: { - size: 'small', - }, - }, MuiInputLabel: { defaultProps: { size: 'small', }, }, - MuiIconButton: { - defaultProps: { - size: 'small', + MuiMenuItem: { + styleOverrides: { + root: { + fontSize: '0.85rem', + }, }, }, - MuiFab: { - defaultProps: { - size: 'small', + MuiDivider: { + styleOverrides: { + root: { + borderColor: hairline, + }, }, }, - MuiCheckbox: { - defaultProps: { - size: 'small', - }, - }, - MuiRadio: { - defaultProps: { - size: 'small', - }, - }, - MuiSwitch: { - defaultProps: { - size: 'small', + MuiDialog: { + styleOverrides: { + paper: { + backgroundColor: surfaceCard, + border: `1px solid ${hairline}`, + boxShadow: '0 12px 32px rgba(20, 20, 19, 0.12)', + }, }, }, MuiDialogTitle: { styleOverrides: { root: { - fontSize: '1.25rem', // Set your desired font size here + color: ink, + }, + }, + }, + MuiDialogContent: { + styleOverrides: { + root: { + color: body, }, }, }, MuiTypography: { styleOverrides: { root: { - color: '#ffffff', // Set the default text color to white + color: ink, }, }, }, - AppBar: { - styleOverrides: { - defaultProps: { - size: 'small', - }, - } - } }, }); -export default theme; \ No newline at end of file +export default theme;