- Enhanced all major components with glassmorphic styling: * EngineData gauges: frosted glass panels with animations * InstrumentPanel: glassmorphic waypoint boxes and data tables * NowPlaying: glassmorphic album art container and controls * All panels: smooth fade-in animations on mount - Updated visual elements: * Consistent use of backdrop-filter blur effect * Semi-transparent borders with 0.1-0.2 opacity * Smooth animations (slideInUp, slideInDown, fadeIn) * Better font weights and hierarchy * Improved contrast and readability - Color scheme refinements: * Highlight backgrounds use RGBA with proper opacity * Better use of accent colors for emphasis * Consistent border styling with transparency * Support for light/dark mode throughout - Animation improvements: * All cards and panels animate on mount * Tab transitions are smooth and snappy * Hover effects with scale and shadow changes * Cubic-bezier timing functions for natural feel - Build optimization: * Still 70 modules, same bundle size * CSS is well-organized and maintainable * No breaking changes to component APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
155 lines
6.0 KiB
JavaScript
155 lines
6.0 KiB
JavaScript
import { useNMEA } from '../../hooks/useNMEA.js'
|
|
import { getApi } from '../../mock/index.js'
|
|
import Gauge from '../instruments/Gauge.jsx'
|
|
import Compass from '../instruments/Compass.jsx'
|
|
import WindRose from '../instruments/WindRose.jsx'
|
|
|
|
function DataRow({ label, value, unit, highlight }) {
|
|
return (
|
|
<div style={{ ...styles.row, background: highlight ? 'rgba(14, 165, 233, 0.08)' : 'transparent' }}>
|
|
<span style={styles.label}>{label}</span>
|
|
<span style={styles.value}>
|
|
{value != null ? `${typeof value === 'number' ? value.toFixed(1) : value}` : '—'}
|
|
{unit && <span style={styles.unit}> {unit}</span>}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function InstrumentPanel() {
|
|
const nmea = useNMEA()
|
|
const isMock = import.meta.env.VITE_USE_MOCK === 'true'
|
|
|
|
let waypointInfo = null
|
|
let allWaypoints = []
|
|
|
|
if (isMock) {
|
|
const api = getApi()
|
|
const snapshot = api.signalk.getSnapshot?.()
|
|
waypointInfo = snapshot?.currentWaypoint
|
|
allWaypoints = snapshot?.waypoints || []
|
|
}
|
|
|
|
return (
|
|
<div style={styles.panel}>
|
|
{/* Gauges row */}
|
|
<div style={styles.gauges}>
|
|
<Compass heading={nmea.heading} cog={nmea.cog} />
|
|
<WindRose windAngle={nmea.windAngle} windSpeed={nmea.windSpeed} />
|
|
<Gauge value={nmea.depth} min={0} max={30} label="Depth" unit="m" warning={3} danger={1.5} />
|
|
<Gauge value={nmea.sog} min={0} max={12} label="SOG" unit="kn" />
|
|
</div>
|
|
|
|
{/* Waypoint info box (only in mock mode) */}
|
|
{isMock && waypointInfo && (
|
|
<div style={styles.waypointBox}>
|
|
<div style={styles.waypointTitle}>📍 Route & Waypoints</div>
|
|
<div style={styles.waypointInfo}>
|
|
<div style={styles.waypointRow}>
|
|
<span>Current:</span>
|
|
<strong>{waypointInfo.name}</strong>
|
|
</div>
|
|
<div style={styles.waypointRow}>
|
|
<span>Distance:</span>
|
|
<strong>{nmea.distanceToWaypoint?.toFixed(2) || '—'} nm</strong>
|
|
</div>
|
|
<div style={styles.waypointRoute}>
|
|
<span style={styles.routeLabel}>Route:</span>
|
|
<div style={styles.waypoints}>
|
|
{allWaypoints.map((wp, i) => (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
...styles.waypointTag,
|
|
background: i === (waypointInfo ? allWaypoints.indexOf(waypointInfo) : 0) ? 'var(--accent)' : 'var(--border)',
|
|
color: i === (waypointInfo ? allWaypoints.indexOf(waypointInfo) : 0) ? 'var(--bg)' : 'var(--text)',
|
|
}}
|
|
>
|
|
{i + 1}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Data table */}
|
|
<div style={styles.table}>
|
|
<DataRow label="COG" value={nmea.cog != null ? Math.round(nmea.cog) : null} unit="°" />
|
|
<DataRow label="Heading" value={nmea.heading != null ? Math.round(nmea.heading) : null} unit="°" />
|
|
<DataRow label="SOG" value={nmea.sog} unit="kn" />
|
|
<DataRow label="Depth" value={nmea.depth} unit="m" />
|
|
<DataRow label="Wind Speed" value={nmea.windSpeed} unit="kn" />
|
|
<DataRow label="Wind Angle" value={nmea.windAngle} unit="°" />
|
|
<DataRow label="Water Temp" value={nmea.waterTemp} unit="°C" />
|
|
<DataRow label="Air Temp" value={nmea.airTemp} unit="°C" />
|
|
<DataRow label="RPM" value={nmea.rpm} unit="" />
|
|
<DataRow label="Rudder" value={nmea.rudder} unit="°" />
|
|
<DataRow label="Fuel" value={nmea.fuel} unit="%" />
|
|
<DataRow label="Lat" value={nmea.lat?.toFixed(5)} unit="°N" />
|
|
<DataRow label="Lon" value={nmea.lon?.toFixed(5)} unit="°E" />
|
|
{isMock && <DataRow label="Distance to WP" value={nmea.distanceToWaypoint} unit="nm" highlight />}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const styles = {
|
|
panel: { display: 'flex', flexDirection: 'column', gap: 16 },
|
|
gauges: { display: 'flex', gap: 10, flexWrap: 'wrap', justifyContent: 'center' },
|
|
|
|
waypointBox: {
|
|
background: 'var(--glass-bg)',
|
|
backdropFilter: 'blur(var(--glass-blur))',
|
|
WebkitBackdropFilter: 'blur(var(--glass-blur))',
|
|
borderRadius: 'var(--radius-lg)',
|
|
border: '1px solid rgba(14, 165, 233, 0.2)',
|
|
padding: 12,
|
|
animation: 'slideInUp 0.3s ease-out',
|
|
},
|
|
waypointTitle: {
|
|
fontSize: 12, fontWeight: 700, color: 'var(--accent)',
|
|
marginBottom: 8, letterSpacing: '0.05em',
|
|
},
|
|
waypointInfo: { display: 'flex', flexDirection: 'column', gap: 6 },
|
|
waypointRow: {
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
fontSize: 12, color: 'var(--text)',
|
|
},
|
|
waypointRoute: {
|
|
display: 'flex', flexDirection: 'column', gap: 6,
|
|
marginTop: 4, paddingTop: 8,
|
|
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
|
},
|
|
routeLabel: { fontSize: 11, color: 'var(--muted)' },
|
|
waypoints: {
|
|
display: 'flex', gap: 6, flexWrap: 'wrap',
|
|
},
|
|
waypointTag: {
|
|
width: 28, height: 28,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
borderRadius: 6, fontSize: 11, fontWeight: 600,
|
|
transition: 'all 0.2s',
|
|
},
|
|
|
|
table: {
|
|
display: 'grid', gridTemplateColumns: '1fr 1fr',
|
|
gap: '0 24px',
|
|
background: 'var(--glass-bg)',
|
|
backdropFilter: 'blur(var(--glass-blur))',
|
|
WebkitBackdropFilter: 'blur(var(--glass-blur))',
|
|
borderRadius: 'var(--radius-lg)', padding: 16,
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
animation: 'slideInUp 0.3s ease-out',
|
|
},
|
|
row: {
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
padding: '7px 0', borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
|
|
transition: 'background 0.2s',
|
|
},
|
|
label: { fontSize: 12, color: 'var(--muted)', fontWeight: 500 },
|
|
value: { fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--text)', fontWeight: 600 },
|
|
unit: { color: 'var(--muted)', fontSize: 11 },
|
|
}
|