Fix navigation chart rendering and add Spotify/AirPlay zone indicators with grouping

- Fixed ChartPlaceholder canvas rendering by replacing CSS variables with hardcoded colors
  * Canvas context cannot use CSS variables; must use RGB/hex colors directly
  * Now renders ship track, waypoints, heading indicator, and legends correctly

- Enhanced ZoneCard with connection indicators
  * Added visual source badges (Spotify 🎵, AirPlay 🎙️, Mopidy 📻)
  * Color-coded left border matching source (green for Spotify, blue for AirPlay, amber for Mopidy)
  * Added source dropdown selector for switching audio sources
  * Shows grouped zone info when zones are merged

- Implemented zone grouping system in ZoneGrid
  * Left sidebar panel to view and manage active zone groups
  * Click 🔗 button to create/remove zone groups
  * When zones are grouped, changing source updates all members
  * Each zone can show which other zones it's grouped with

- All 64 modules build successfully
- Mock data properly flows through Snapcast with Spotify/AirPlay/Mopidy sources

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-27 14:45:22 +01:00
parent 19b2c30a0a
commit 99a1aa6460
3 changed files with 247 additions and 27 deletions

View File

@@ -11,6 +11,17 @@ export default function ChartPlaceholder() {
const chartUrl = `http://${signalkHost}:3000/@signalk/freeboard-sk/`
const isMock = import.meta.env.VITE_USE_MOCK === 'true'
// Colors from CSS (hardcoded for canvas compatibility)
const colors = {
bg: '#07111f',
surface: '#0a1928',
border: '#1e2a3a',
text: '#e2eaf2',
muted: '#4a6080',
accent: '#38bdf8',
warning: '#f59e0b',
}
// Draw track and waypoints
useEffect(() => {
const canvas = canvasRef.current
@@ -68,9 +79,9 @@ export default function ChartPlaceholder() {
})
// Clear and draw background
ctx.fillStyle = 'var(--surface)'
ctx.fillStyle = colors.surface
ctx.fillRect(0, 0, width, height)
ctx.strokeStyle = 'var(--border)'
ctx.strokeStyle = colors.border
ctx.lineWidth = 1
ctx.strokeRect(0, 0, width, height)
@@ -79,14 +90,14 @@ export default function ChartPlaceholder() {
const p = project(wp.lat, wp.lon)
// Waypoint circle
ctx.fillStyle = idx === snapshot?.currentWaypoint ? 'var(--accent)' : 'var(--warning)'
ctx.fillStyle = idx === snapshot?.currentWaypoint ? colors.accent : colors.warning
ctx.beginPath()
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2)
ctx.fill()
// Waypoint number
ctx.fillStyle = 'var(--bg)'
ctx.font = 'bold 10px var(--font-mono)'
ctx.fillStyle = colors.bg
ctx.font = 'bold 10px monospace'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(idx + 1, p.x, p.y)
@@ -94,7 +105,7 @@ export default function ChartPlaceholder() {
// Draw track line
if (trackRef.current.length > 1) {
ctx.strokeStyle = 'var(--accent)'
ctx.strokeStyle = colors.accent
ctx.lineWidth = 2
ctx.beginPath()
trackRef.current.forEach((p, idx) => {
@@ -116,7 +127,7 @@ export default function ChartPlaceholder() {
ctx.save()
ctx.translate(p.x, p.y)
ctx.rotate(headRad)
ctx.fillStyle = 'var(--accent)'
ctx.fillStyle = colors.accent
ctx.beginPath()
ctx.moveTo(0, -size)
ctx.lineTo(-size / 2, size / 2)
@@ -126,7 +137,7 @@ export default function ChartPlaceholder() {
ctx.restore()
// Ship circle
ctx.strokeStyle = 'var(--accent)'
ctx.strokeStyle = colors.accent
ctx.lineWidth = 2
ctx.beginPath()
ctx.arc(p.x, p.y, 8, 0, Math.PI * 2)