diff --git a/dashboard/src/components/audio/ZoneCard.jsx b/dashboard/src/components/audio/ZoneCard.jsx index 331a859..901b68e 100644 --- a/dashboard/src/components/audio/ZoneCard.jsx +++ b/dashboard/src/components/audio/ZoneCard.jsx @@ -1,22 +1,67 @@ -export default function ZoneCard({ zone, onVolume, onMute, onSource }) { +export default function ZoneCard({ zone, onVolume, onMute, onSource, onGroup, groupedWith }) { const { id, name, active, volume, muted, source } = zone + // Map source to emoji and connection info + const sourceInfo = { + 'Spotify': { emoji: '🎡', color: '#1DB954', label: 'Spotify' }, + 'AirPlay': { emoji: 'πŸŽ™οΈ', color: '#60a5fa', label: 'AirPlay' }, + 'Mopidy': { emoji: 'πŸ“»', color: '#f59e0b', label: 'Mopidy' }, + } + const info = sourceInfo[source] || { emoji: 'πŸ“’', color: 'var(--muted)', label: source } + return (
- {name} +
+ {name} + + {info.emoji} {info.label} + +
- {active ? 'ON' : 'OFF'} + {active ? 'βœ“' : 'β—‹'}
-
{source}
+ {groupedWith && groupedWith.length > 0 && ( +
+ Grouped with: +
+ {groupedWith.map(zoneName => ( + {zoneName} + ))} +
+
+ )} + +
+ + {onGroup && ( + + )} +
+
+ ) + })} +
+ )} + + +
+ {zones.map(zone => ( + handleSourceChange(id, source)} + onGroup={handleGroupToggle} + groupedWith={getGroupedWith(zone.id)} + /> + ))} +
) } const styles = { + container: { + display: 'grid', + gridTemplateColumns: '240px 1fr', + gap: 16, + height: '100%', + }, + groupingPanel: { + background: 'var(--surface)', + borderRadius: 'var(--radius)', + border: '1px solid var(--border)', + padding: 12, + display: 'flex', + flexDirection: 'column', + gap: 10, + maxHeight: '100vh', + overflowY: 'auto', + }, + panelTitle: { + fontSize: 12, + fontWeight: 700, + color: 'var(--muted)', + margin: '0 0 8px 0', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + emptyText: { + fontSize: 11, + color: 'var(--muted)', + textAlign: 'center', + padding: '12px 0', + }, + groupList: { + display: 'flex', + flexDirection: 'column', + gap: 6, + }, + groupListItem: { + background: '#38bdf811', + border: '1px solid var(--accent)', + borderRadius: 4, + padding: '8px 10px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, + groupName: { + fontSize: 11, + fontWeight: 600, + color: 'var(--accent)', + flex: 1, + }, + ungroupBtn: { + background: 'none', + border: 'none', + color: 'var(--accent)', + cursor: 'pointer', + fontSize: 14, + padding: '0 4px', + minWidth: 24, + minHeight: 24, + }, grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 12, + overflowY: 'auto', } } diff --git a/dashboard/src/components/nav/ChartPlaceholder.jsx b/dashboard/src/components/nav/ChartPlaceholder.jsx index 7f63aa8..27a7c51 100644 --- a/dashboard/src/components/nav/ChartPlaceholder.jsx +++ b/dashboard/src/components/nav/ChartPlaceholder.jsx @@ -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)