diff --git a/dashboard/src/lib/components/ModelCard.svelte b/dashboard/src/lib/components/ModelCard.svelte index 9046d2a6..7d3056da 100644 --- a/dashboard/src/lib/components/ModelCard.svelte +++ b/dashboard/src/lib/components/ModelCard.svelte @@ -6,6 +6,27 @@ TopologyEdge, } from "$lib/stores/app.svelte"; import { debugMode, topologyData } from "$lib/stores/app.svelte"; + import { theme } from "$lib/stores/theme.svelte"; + + // Theme-aware colors for SVG elements + let tc = $derived({ + accent: theme.isLight ? "#374151" : "#FFD700", + accentDim: theme.isLight ? "rgba(26,26,26,0.06)" : "rgba(255,215,0,0.1)", + accentGlow: theme.isLight ? "rgba(0,0,0,0.03)" : "rgba(255,215,0,0.1)", + outlineActive: theme.isLight ? "#374151" : "#FFD700", + outlineInactive: theme.isLight ? "#d1d5db" : "#4B5563", + strokeActive: theme.isLight ? "#374151" : "#FFD700", + strokeInactive: theme.isLight ? "#d1d5db" : "#4B5563", + caseFill: theme.isLight ? "#f3f4f6" : "#0a0a0a", + caseDetail: theme.isLight ? "#d1d5db" : "#374151", + errorDot: theme.isLight ? "#dc2626" : "#f87171", + statusInactive: theme.isLight ? "#9ca3af" : "#4B5563", + connGood: theme.isLight ? "rgba(30,30,30,0.85)" : "rgba(255,255,255,0.85)", + connBad: theme.isLight ? "rgba(220,38,38,0.85)" : "rgba(248,113,113,0.85)", + scanlineColor: theme.isLight + ? "rgba(0,0,0,0.01)" + : "rgba(255,215,0,0.02)", + }); interface Props { model: { id: string; name?: string; storage_size_megabytes?: number }; @@ -491,7 +512,7 @@
@@ -589,7 +610,8 @@ >
{conn.arrow} {isRdma @@ -725,9 +747,7 @@ dominant-baseline="hanging" font-size="6" font-family="SF Mono, Monaco, monospace" - fill={conn.iface - ? "rgba(255,255,255,0.85)" - : "rgba(248,113,113,0.85)"} + fill={conn.iface ? tc.connGood : tc.connBad} > {conn.arrow} {isRdma @@ -746,9 +766,7 @@ dominant-baseline="auto" font-size="6" font-family="SF Mono, Monaco, monospace" - fill={conn.iface - ? "rgba(255,255,255,0.85)" - : "rgba(248,113,113,0.85)"} + fill={conn.iface ? tc.connGood : tc.connBad} > {conn.arrow} {isRdma @@ -767,9 +785,7 @@ dominant-baseline="auto" font-size="6" font-family="SF Mono, Monaco, monospace" - fill={conn.iface - ? "rgba(255,255,255,0.85)" - : "rgba(248,113,113,0.85)"} + fill={conn.iface ? tc.connGood : tc.connBad} > {conn.arrow} {isRdma @@ -801,7 +817,7 @@ height={node.iconSize * 0.65} rx="2" fill="none" - stroke={node.isUsed ? "#FFD700" : "#4B5563"} + stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive} stroke-width="1.5" /> @@ -810,7 +826,7 @@ y="2" width={node.iconSize - 8} height={node.screenHeight} - fill="#0a0a0a" + fill={tc.caseFill} /> {#if node.modelUsageGB > 0 && node.isUsed} @@ -830,7 +846,7 @@ node.modelFillHeight} width={node.iconSize - 8} height={node.modelFillHeight} - fill="#FFD700" + fill={tc.accent} filter="url(#memGlow-{filterId})" class="animate-pulse-slow" /> @@ -842,7 +858,7 @@ 0.68} L {node.iconSize - 2} {node.iconSize * 0.78} L 2 {node.iconSize * 0.78} Z" fill="none" - stroke={node.isUsed ? "#FFD700" : "#4B5563"} + stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive} stroke-width="1.5" /> @@ -859,7 +875,7 @@ height={node.iconSize - 4} rx="4" fill="none" - stroke={node.isUsed ? "#FFD700" : "#4B5563"} + stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive} stroke-width="1.5" /> @@ -868,7 +884,7 @@ y="4" width={node.iconSize - 8} height={node.iconSize - 8} - fill="#0a0a0a" + fill={tc.caseFill} /> {#if node.modelUsageGB > 0 && node.isUsed} @@ -887,7 +903,7 @@ width={node.iconSize - 8} height={(node.iconSize - 8) * ((node.newPercent - node.currentPercent) / 100)} - fill="#FFD700" + fill={tc.accent} filter="url(#memGlow-{filterId})" class="animate-pulse-slow" /> @@ -906,7 +922,7 @@ height={node.iconSize * 0.4} rx="3" fill="none" - stroke={node.isUsed ? "#FFD700" : "#4B5563"} + stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive} stroke-width="1.5" /> @@ -915,7 +931,7 @@ y={node.iconSize * 0.32} width={node.iconSize - 8} height={node.iconSize * 0.36} - fill="#0a0a0a" + fill={tc.caseFill} /> {#if node.modelUsageGB > 0 && node.isUsed} @@ -936,7 +952,7 @@ height={node.iconSize * 0.36 * ((node.newPercent - node.currentPercent) / 100)} - fill="#FFD700" + fill={tc.accent} filter="url(#memGlow-{filterId})" class="animate-pulse-slow" /> @@ -955,8 +971,8 @@ 0.75} {node.iconSize / 2},{node.iconSize} 0,{node.iconSize * 0.75} 0,{node.iconSize * 0.25}" - fill={node.isUsed ? "rgba(255,215,0,0.1)" : "#0a0a0a"} - stroke={node.isUsed ? "#FFD700" : "#4B5563"} + fill={node.isUsed ? tc.accentDim : tc.caseFill} + stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive} stroke-width="1.5" /> @@ -970,9 +986,9 @@ font-family="SF Mono, Monaco, monospace" fill={node.isUsed ? node.newPercent > 90 - ? "#f87171" - : "#FFD700" - : "#4B5563"} + ? tc.errorDot + : tc.accent + : tc.statusInactive} > {node.newPercent.toFixed(0)}% diff --git a/dashboard/src/lib/components/TopologyGraph.svelte b/dashboard/src/lib/components/TopologyGraph.svelte index 0d0d3c3d..3be960cd 100644 --- a/dashboard/src/lib/components/TopologyGraph.svelte +++ b/dashboard/src/lib/components/TopologyGraph.svelte @@ -10,6 +10,53 @@ nodeIdentities, type NodeInfo, } from "$lib/stores/app.svelte"; + import { theme } from "$lib/stores/theme.svelte"; + + // Theme-aware colors for D3-rendered SVG elements + let themeColors = $derived({ + accent: theme.isLight ? "oklch(0.50 0.14 65)" : "oklch(0.85 0.18 85)", + accentRgb: theme.isLight ? "55,40,15" : "255,215,0", + deviceCase: theme.isLight ? "#e8e8e8" : "#1a1a1a", + deviceCaseDark: theme.isLight ? "#d4d4d4" : "#2c2c2c", + deviceScreen: theme.isLight ? "#f0f0f5" : "#0a0a12", + deviceScreenFill: theme.isLight + ? "rgba(240,242,248,0.95)" + : "rgba(0,20,40,0.9)", + labelWhite: theme.isLight ? "#1a1a1a" : "#FFFFFF", + labelMuted: theme.isLight ? "rgba(80,80,80,0.9)" : "rgba(179,179,179,0.9)", + labelDim: theme.isLight ? "rgba(100,100,100,0.7)" : "rgba(179,179,179,0.7)", + wireDefault: theme.isLight + ? "rgba(120,120,120,0.6)" + : "rgba(179,179,179,0.8)", + wireBright: theme.isLight ? "rgba(30,30,30,0.9)" : "rgba(255,255,255,0.9)", + wireFiltered: theme.isLight + ? "rgba(160,160,160,0.5)" + : "rgba(140,140,140,0.6)", + gridStroke: theme.isLight + ? "var(--exo-light-gray, #888888)" + : "var(--exo-light-gray, #B3B3B3)", + errorText: theme.isLight + ? "rgba(220,38,38,0.9)" + : "rgba(248,113,113,0.9)", + normalText: theme.isLight + ? "rgba(30,30,30,0.85)" + : "rgba(255,255,255,0.85)", + gpuChip: theme.isLight + ? "rgba(180, 180, 190, 0.7)" + : "rgba(80, 80, 90, 0.7)", + detailOverlay: theme.isLight ? "rgba(0,0,0,0.08)" : "rgba(0,0,0,0.35)", + deviceShadow: theme.isLight ? "rgba(0,0,0,0.06)" : "rgba(0,0,0,0.2)", + deviceHighlight: theme.isLight + ? "rgba(255,255,255,0.5)" + : "rgba(255,255,255,0.08)", + tbActive: theme.isLight + ? "rgba(30,28,20,0.9)" + : "rgba(234,179,8,0.9)", + tbInactive: theme.isLight + ? "rgba(160,160,160,0.7)" + : "rgba(100,100,100,0.7)", + deviceDetail: theme.isLight ? "#c0c0c0" : "#374151", + }); interface Props { class?: string; @@ -127,7 +174,7 @@ function getTemperatureColor(temp: number): string { // Default for N/A temp - light gray - if (isNaN(temp) || temp === null) return "rgba(179, 179, 179, 0.8)"; + if (isNaN(temp) || temp === null) return themeColors.wireDefault; const coolTemp = 45; // Temp for pure blue const midTemp = 57.5; // Temp for pure yellow @@ -208,7 +255,7 @@ .append("path") .attr("d", "M 0 0 L 10 5 L 0 10") .attr("fill", "none") - .attr("stroke", "var(--exo-light-gray, #B3B3B3)") + .attr("stroke", themeColors.gridStroke) .attr("stroke-width", "1.6") .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") @@ -221,7 +268,7 @@ .attr("y", centerY) .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") - .attr("fill", "rgba(255,215,0,0.4)") + .attr("fill", `rgba(${themeColors.accentRgb},0.4)`) .attr("font-size", isMinimized ? 10 : 12) .attr("font-family", "SF Mono, monospace") .attr("letter-spacing", "0.1em") @@ -505,8 +552,8 @@ .attr( "fill", conn.missingIface - ? "rgba(248,113,113,0.9)" - : "rgba(255,255,255,0.85)", + ? themeColors.errorText + : themeColors.normalText, ) .text(label); currentY += isTop ? lineHeight : -lineHeight; @@ -563,24 +610,24 @@ filteredNodes.size > 0 && !filteredNodes.has(nodeInfo.id); const isHovered = hoveredNodeId === nodeInfo.id && !isInFilter; - // Holographic wireframe colors - bright yellow for filter, subtle yellow for hover, grey for filtered out + // Holographic wireframe colors - accent for filter, subtle for hover, grey for filtered out const wireColor = isInFilter - ? "rgba(255,215,0,1)" // Bright yellow for filter selection + ? `rgba(${themeColors.accentRgb},1)` : isHovered - ? "rgba(255,215,0,0.7)" // Subtle yellow for hover + ? `rgba(${themeColors.accentRgb},0.7)` : isHighlighted - ? "rgba(255,215,0,0.9)" // Yellow for instance highlight + ? `rgba(${themeColors.accentRgb},0.9)` : isFilteredOut - ? "rgba(140,140,140,0.6)" // Grey for filtered out - : "rgba(179,179,179,0.8)"; // Default - const wireColorBright = "rgba(255,255,255,0.9)"; + ? themeColors.wireFiltered + : themeColors.wireDefault; + const wireColorBright = themeColors.wireBright; const fillColor = isInFilter - ? "rgba(255,215,0,0.25)" + ? `rgba(${themeColors.accentRgb},0.25)` : isHovered - ? "rgba(255,215,0,0.12)" + ? `rgba(${themeColors.accentRgb},0.12)` : isHighlighted - ? "rgba(255,215,0,0.15)" - : "rgba(255,215,0,0.08)"; + ? `rgba(${themeColors.accentRgb},0.15)` + : `rgba(${themeColors.accentRgb},0.08)`; const strokeWidth = isInFilter ? 3 : isHovered @@ -588,8 +635,8 @@ : isHighlighted ? 2.5 : 1.5; - const screenFill = "rgba(0,20,40,0.9)"; - const glowColor = "rgba(255,215,0,0.3)"; + const screenFill = themeColors.deviceScreenFill; + const glowColor = `rgba(${themeColors.accentRgb},0.3)`; const nodeG = nodesGroup .append("g") @@ -653,7 +700,7 @@ .attr("width", iconBaseWidth) .attr("height", iconBaseHeight) .attr("rx", cornerRadius) - .attr("fill", "#1a1a1a") + .attr("fill", themeColors.deviceCase) .attr("stroke", wireColor) .attr("stroke-width", strokeWidth); @@ -671,12 +718,12 @@ ) .attr("width", iconBaseWidth) .attr("height", memFillActualHeight) - .attr("fill", "rgba(255,215,0,0.75)") + .attr("fill", `rgba(${themeColors.accentRgb},0.75)`) .attr("clip-path", `url(#${studioClipId})`); } // Front panel details - vertical slots - const detailColor = "rgba(0,0,0,0.35)"; + const detailColor = themeColors.detailOverlay; const slotHeight = iconBaseHeight * 0.14; const vSlotWidth = iconBaseWidth * 0.05; const vSlotY = @@ -736,7 +783,7 @@ .attr("width", iconBaseWidth) .attr("height", iconBaseHeight) .attr("rx", cornerRadius) - .attr("fill", "#1a1a1a") + .attr("fill", themeColors.deviceCase) .attr("stroke", wireColor) .attr("stroke-width", strokeWidth); @@ -754,12 +801,12 @@ ) .attr("width", iconBaseWidth) .attr("height", memFillActualHeight) - .attr("fill", "rgba(255,215,0,0.75)") + .attr("fill", `rgba(${themeColors.accentRgb},0.75)`) .attr("clip-path", `url(#${miniClipId})`); } // Front panel details - vertical slots (no horizontal slot for Mini) - const detailColor = "rgba(0,0,0,0.35)"; + const detailColor = themeColors.detailOverlay; const slotHeight = iconBaseHeight * 0.2; const vSlotWidth = iconBaseWidth * 0.045; const vSlotY = @@ -814,7 +861,7 @@ .attr("width", screenWidth) .attr("height", screenHeight) .attr("rx", 3) - .attr("fill", "#1a1a1a") + .attr("fill", themeColors.deviceCase) .attr("stroke", wireColor) .attr("stroke-width", strokeWidth); @@ -826,7 +873,7 @@ .attr("width", screenWidth - screenBezel * 2) .attr("height", screenHeight - screenBezel * 2) .attr("rx", 2) - .attr("fill", "#0a0a12"); + .attr("fill", themeColors.deviceScreen); // Memory fill on screen (fills from bottom up - classic style) if (ramUsagePercent > 0) { @@ -842,7 +889,7 @@ ) .attr("width", screenWidth - screenBezel * 2) .attr("height", memFillActualHeight) - .attr("fill", "rgba(255,215,0,0.85)") + .attr("fill", `rgba(${themeColors.accentRgb},0.85)`) .attr("clip-path", `url(#${screenClipId})`); } @@ -859,7 +906,7 @@ "transform", `translate(${logoX}, ${logoY}) scale(${logoScale})`, ) - .attr("fill", "#FFFFFF") + .attr("fill", themeColors.labelWhite) .attr("opacity", 0.9); // Base (keyboard) - trapezoidal @@ -875,7 +922,7 @@ "d", `M ${baseTopX} ${baseY} L ${baseTopX + baseTopWidth} ${baseY} L ${baseBottomX + baseBottomWidth} ${baseY + baseHeight} L ${baseBottomX} ${baseY + baseHeight} Z`, ) - .attr("fill", "#2c2c2c") + .attr("fill", themeColors.deviceCaseDark) .attr("stroke", wireColor) .attr("stroke-width", 1); @@ -890,7 +937,7 @@ .attr("y", keyboardY) .attr("width", keyboardWidth) .attr("height", keyboardHeight) - .attr("fill", "rgba(0,0,0,0.2)") + .attr("fill", themeColors.deviceShadow) .attr("rx", 2); // Trackpad @@ -904,7 +951,7 @@ .attr("y", trackpadY) .attr("width", trackpadWidth) .attr("height", trackpadHeight) - .attr("fill", "rgba(255,255,255,0.08)") + .attr("fill", themeColors.deviceHighlight) .attr("rx", 2); } else { // Default/Unknown - holographic hexagon @@ -942,7 +989,7 @@ .attr("y", gpuBarY) .attr("width", gpuBarWidth) .attr("height", gpuBarHeight) - .attr("fill", "rgba(80, 80, 90, 0.7)") + .attr("fill", themeColors.gpuChip) .attr("rx", 2); // GPU Bar Fill (from bottom up, colored by temperature) @@ -979,7 +1026,7 @@ .attr("y", gpuTextY - lineSpacing) .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") - .attr("fill", "#FFFFFF") + .attr("fill", themeColors.labelWhite) .attr("font-size", gpuTextFontSize) .attr("font-weight", "700") .attr("font-family", "SF Mono, Monaco, monospace") @@ -992,7 +1039,7 @@ .attr("y", gpuTextY) .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") - .attr("fill", "#FFFFFF") + .attr("fill", themeColors.labelWhite) .attr("font-size", gpuTextFontSize) .attr("font-weight", "700") .attr("font-family", "SF Mono, Monaco, monospace") @@ -1005,7 +1052,7 @@ .attr("y", gpuTextY + lineSpacing) .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") - .attr("fill", "#FFFFFF") + .attr("fill", themeColors.labelWhite) .attr("font-size", gpuTextFontSize) .attr("font-weight", "700") .attr("font-family", "SF Mono, Monaco, monospace") @@ -1033,7 +1080,7 @@ .attr("y", nameY) .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") - .attr("fill", "#FFD700") + .attr("fill", themeColors.accent) .attr("font-size", fontSize) .attr("font-weight", 500) .attr("font-family", "SF Mono, Monaco, monospace") @@ -1050,15 +1097,15 @@ .attr("font-family", "SF Mono, Monaco, monospace"); memText .append("tspan") - .attr("fill", "rgba(255,215,0,0.9)") + .attr("fill", `rgba(${themeColors.accentRgb},0.9)`) .text(`${formatBytes(ramUsed)}`); memText .append("tspan") - .attr("fill", "rgba(179,179,179,0.9)") + .attr("fill", themeColors.labelMuted) .text(`/${formatBytes(ramTotal)}`); memText .append("tspan") - .attr("fill", "rgba(179,179,179,0.7)") + .attr("fill", themeColors.labelDim) .text(` (${ramUsagePercent.toFixed(0)}%)`); } else if (showCompactLabels) { // COMPACT MODE: Just name and basic info (4+ nodes) @@ -1075,7 +1122,7 @@ .attr("x", nodeInfo.x) .attr("y", nameY) .attr("text-anchor", "middle") - .attr("fill", "#FFD700") + .attr("fill", themeColors.accent) .attr("font-size", fontSize) .attr("font-family", "SF Mono, Monaco, monospace") .text(shortName); @@ -1087,7 +1134,7 @@ .attr("x", nodeInfo.x) .attr("y", statsY) .attr("text-anchor", "middle") - .attr("fill", "rgba(255,215,0,0.7)") + .attr("fill", `rgba(${themeColors.accentRgb},0.7)`) .attr("font-size", fontSize * 0.85) .attr("font-family", "SF Mono, Monaco, monospace") .text( @@ -1108,7 +1155,7 @@ .attr("x", nodeInfo.x) .attr("y", nameY) .attr("text-anchor", "middle") - .attr("fill", "#FFD700") + .attr("fill", themeColors.accent) .attr("font-size", fontSize) .attr("font-weight", "500") .attr("font-family", "SF Mono, Monaco, monospace") @@ -1125,15 +1172,15 @@ .attr("font-family", "SF Mono, Monaco, monospace"); memTextMini .append("tspan") - .attr("fill", "rgba(255,215,0,0.9)") + .attr("fill", `rgba(${themeColors.accentRgb},0.9)`) .text(`${formatBytes(ramUsed)}`); memTextMini .append("tspan") - .attr("fill", "rgba(179,179,179,0.9)") + .attr("fill", themeColors.labelMuted) .text(`/${formatBytes(ramTotal)}`); memTextMini .append("tspan") - .attr("fill", "rgba(179,179,179,0.7)") + .attr("fill", themeColors.labelDim) .text(` (${ramUsagePercent.toFixed(0)}%)`); } @@ -1149,8 +1196,8 @@ const tbStatus = tbBridgeData[nodeInfo.id]; if (tbStatus) { const tbColor = tbStatus.enabled - ? "rgba(234,179,8,0.9)" - : "rgba(100,100,100,0.7)"; + ? themeColors.tbActive + : themeColors.tbInactive; const tbText = tbStatus.enabled ? "TB:ON" : "TB:OFF"; nodeG .append("text") @@ -1168,7 +1215,7 @@ if (rdmaStatus !== undefined) { const rdmaColor = rdmaStatus.enabled ? "rgba(74,222,128,0.9)" - : "rgba(100,100,100,0.7)"; + : themeColors.tbInactive; const rdmaText = rdmaStatus.enabled ? "RDMA:ON" : "RDMA:OFF"; nodeG .append("text") @@ -1189,7 +1236,7 @@ .attr("x", nodeInfo.x) .attr("y", debugLabelY) .attr("text-anchor", "middle") - .attr("fill", "rgba(179,179,179,0.7)") + .attr("fill", themeColors.labelDim) .attr("font-size", debugFontSize) .attr("font-family", "SF Mono, Monaco, monospace") .text( @@ -1206,6 +1253,7 @@ const _hoveredNodeId = hoveredNodeId; const _filteredNodes = filteredNodes; const _highlightedNodes = highlightedNodes; + const _themeColors = themeColors; if (_data) { renderGraph(); }