Compare commits

...

2 Commits

Author SHA1 Message Date
Alex Cheema 99b0db0634 chore: apply nix fmt formatting to svelte files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 11:46:00 -08:00
Alex Cheema e3d91dc08a feat: add light/dark mode toggle to dashboard
Wires up the existing mode-watcher dependency (added in #1017 but never
used) to provide a one-click light/dark toggle in the header nav.

Changes:
- app.css: Add full light theme via html:not(.dark) block overriding all
  --exo-* brand color variables; add utility overrides for text-white,
  bg-black, scrollbar, command-panel, glow-text, graph links, code
  blocks, hljs syntax, KaTeX/math, and LaTeX proof/theorem blocks.
  Fix --exo-glow-yellow to be a color value (not a shadow value) so it
  works inside Tailwind arbitrary shadow classes.
- app.html: Add class="dark" to <html> for FOUC prevention.
- +layout.svelte: Add <ModeWatcher defaultMode="dark" />.
- HeaderNav.svelte: Add sun/moon toggle button using toggleMode from
  mode-watcher; use color-mix() for logo drop-shadow.
- TopologyGraph.svelte: Add getThemeColors() with MutationObserver on
  <html> class for reactive D3 color updates.
- ModelCard.svelte: Add isDark state + MutationObserver + tc derived
  color map; replace hardcoded SVG device colors with theme-aware values.
- MarkdownContent.svelte: Add scoped light mode overrides for markdown
  rendering (code blocks, tables, blockquotes, math).
- ChatForm, ChatMessages, ChatAttachments: Replace hardcoded rgba shadows
  with var(--exo-glow-yellow).
- ImageParamsPanel: Replace #ffd700 slider thumb colors with var(--exo-yellow).
- downloads/+page.svelte: Replace hardcoded hover bg with var(--exo-bg-hover).

Dark mode remains the default. Builds with zero errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 11:04:09 -08:00
13 changed files with 963 additions and 185 deletions
+384 -2
View File
@@ -16,8 +16,8 @@
/* Gotham-inspired accent colors */
--exo-grid: oklch(0.25 0 0);
--exo-scanline: oklch(0.15 0 0);
--exo-glow-yellow: 0 0 20px oklch(0.85 0.18 85 / 0.3);
--exo-glow-yellow-strong: 0 0 40px oklch(0.85 0.18 85 / 0.5);
--exo-glow-yellow: oklch(0.85 0.18 85 / 0.3);
--exo-glow-yellow-strong: oklch(0.85 0.18 85 / 0.5);
/* Theme Variables */
--radius: 0.375rem;
@@ -39,6 +39,67 @@
--border: oklch(0.22 0 0);
--input: oklch(0.22 0 0);
--ring: var(--exo-yellow);
/* Adaptive text colors (used via utility classes below) */
--text-primary: oklch(0.9 0 0);
--text-secondary: oklch(0.9 0 0 / 0.9);
--text-muted: oklch(0.9 0 0 / 0.7);
--text-faint: oklch(0.9 0 0 / 0.5);
--text-ghost: oklch(0.9 0 0 / 0.4);
--text-subtle: oklch(0.9 0 0 / 0.6);
--text-dim: oklch(0.9 0 0 / 0.3);
--text-whisper: oklch(0.9 0 0 / 0.8);
--exo-bg-hover: oklch(0.18 0 0);
}
/* ============================================================
LIGHT THEME - Applied when html does NOT have .dark class
============================================================ */
html:not(.dark) {
/* Override EXO brand colors for light mode */
--exo-black: oklch(0.97 0 0);
--exo-dark-gray: oklch(0.94 0 0);
--exo-medium-gray: oklch(0.88 0 0);
--exo-light-gray: oklch(0.45 0 0);
--exo-yellow: oklch(0.20 0.02 85);
--exo-yellow-darker: oklch(0.15 0.02 85);
--exo-yellow-glow: oklch(0.30 0.03 85);
--exo-grid: oklch(0.88 0 0);
--exo-scanline: oklch(0.92 0 0);
--exo-glow-yellow: oklch(0.20 0.02 85 / 0.08);
--exo-glow-yellow-strong: oklch(0.20 0.02 85 / 0.12);
/* Theme Variables */
--background: oklch(0.97 0 0);
--foreground: oklch(0.15 0 0);
--card: oklch(0.94 0 0);
--card-foreground: oklch(0.15 0 0);
--popover: oklch(0.96 0 0);
--popover-foreground: oklch(0.15 0 0);
--primary: oklch(0.20 0.02 85);
--primary-foreground: oklch(0.97 0 0);
--secondary: oklch(0.90 0 0);
--secondary-foreground: oklch(0.15 0 0);
--muted: oklch(0.92 0 0);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(0.90 0 0);
--accent-foreground: oklch(0.15 0 0);
--destructive: oklch(0.55 0.25 25);
--border: oklch(0.85 0 0);
--input: oklch(0.88 0 0);
--ring: oklch(0.20 0.02 85);
/* Adaptive text colors for light mode */
--text-primary: oklch(0.15 0 0);
--text-secondary: oklch(0.15 0 0 / 0.9);
--text-muted: oklch(0.15 0 0 / 0.7);
--text-faint: oklch(0.15 0 0 / 0.5);
--text-ghost: oklch(0.15 0 0 / 0.4);
--text-subtle: oklch(0.15 0 0 / 0.6);
--text-dim: oklch(0.15 0 0 / 0.3);
--text-whisper: oklch(0.15 0 0 / 0.8);
--exo-bg-hover: oklch(0.90 0 0);
}
@theme inline {
@@ -266,6 +327,327 @@ input:focus, textarea:focus {
box-shadow: none;
}
/* ============================================================
LIGHT MODE: Override hardcoded white/black text colors
Components use text-white/XX extensively - in light mode these
need to become dark text. We override at the CSS level so we
don't have to touch every single component.
============================================================ */
html:not(.dark) {
/* text-white variants → dark text */
& .text-white\/90,
& .text-white\/80,
& .text-white\/70,
& .text-white\/60 {
color: var(--foreground) !important;
}
/* Muted text-white variants → dark text with reduced alpha via color-mix */
& .text-white\/50 {
color: color-mix(in oklch, var(--foreground) 50%, transparent) !important;
}
& .text-white\/40 {
color: color-mix(in oklch, var(--foreground) 40%, transparent) !important;
}
& .text-white\/30 {
color: color-mix(in oklch, var(--foreground) 30%, transparent) !important;
}
/* bg-black variants → light bg */
& .bg-black\/40,
& .bg-black\/50,
& .bg-black\/60,
& .bg-black\/80 {
background-color: oklch(0.92 0 0 / 0.5) !important;
}
/* bg-exo-black opacity variants */
& [class*="bg-exo-black/"] {
background-color: oklch(0.92 0 0 / 0.5) !important;
}
/* shadow-black → softer shadow */
& [class*="shadow-black"] {
--tw-shadow-color: oklch(0 0 0 / 0.08) !important;
}
/* Scrollbar light mode */
& ::-webkit-scrollbar-track {
background: oklch(0.94 0 0) !important;
}
& ::-webkit-scrollbar-thumb {
background: oklch(0.78 0 0) !important;
}
& ::-webkit-scrollbar-thumb:hover {
background: oklch(0.40 0 0) !important;
}
/* Command panel light mode */
& .command-panel {
background: linear-gradient(
180deg,
oklch(0.96 0 0 / 0.95) 0%,
oklch(0.94 0 0 / 0.98) 100%
) !important;
border-color: oklch(0.85 0 0) !important;
box-shadow:
inset 0 1px 0 oklch(1 0 0 / 0.5),
0 4px 20px oklch(0 0 0 / 0.08) !important;
}
/* Glow text - no glow in light mode, just subtle shadow */
& .glow-text {
text-shadow:
0 0 10px oklch(0 0 0 / 0.06),
0 0 20px oklch(0 0 0 / 0.03) !important;
}
/* Grid bg */
& .grid-bg {
background-image:
linear-gradient(oklch(0.85 0 0 / 0.3) 1px, transparent 1px),
linear-gradient(90deg, oklch(0.85 0 0 / 0.3) 1px, transparent 1px) !important;
}
/* Scanlines - very subtle in light mode */
& .scanlines::before {
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
oklch(0 0 0 / 0.015) 2px,
oklch(0 0 0 / 0.015) 4px
) !important;
}
/* CRT screen effect */
& .crt-screen {
background: radial-gradient(
ellipse at center,
oklch(0.96 0 0) 0%,
oklch(0.94 0 0) 50%,
oklch(0.92 0 0) 100%
) !important;
box-shadow:
inset 0 0 100px oklch(0 0 0 / 0.05),
0 0 50px oklch(0 0 0 / 0.03) !important;
}
/* Graph links */
& .graph-link {
stroke: oklch(0.35 0 0 / 0.5) !important;
filter: none !important;
}
& .graph-link-active {
stroke: oklch(0.20 0 0 / 0.8) !important;
filter: none !important;
}
/* Shooting stars - hide in light mode */
& .shooting-stars {
display: none !important;
}
/* Logo: invert white→black + subtle dark shadow */
& img[alt="EXO"] {
filter: invert(1) drop-shadow(0 0 10px oklch(0 0 0 / 0.15)) !important;
}
/* Status indicator colors */
& .bg-exo-yellow.rounded-full {
box-shadow: 0 0 6px oklch(0 0 0 / 0.15);
}
/* Red/green/blue semantic colors stay mostly the same but darken slightly */
& .text-red-400 {
color: oklch(0.55 0.22 25) !important;
}
& .text-green-400 {
color: oklch(0.50 0.18 155) !important;
}
& .text-blue-200 {
color: oklch(0.50 0.16 250) !important;
}
& .text-blue-300 {
color: oklch(0.50 0.17 250) !important;
}
& .text-blue-400 {
color: oklch(0.50 0.18 250) !important;
}
/* bg-red variants */
& .bg-red-500\/10 {
background-color: oklch(0.55 0.22 25 / 0.08) !important;
}
& .bg-red-500\/20 {
background-color: oklch(0.55 0.22 25 / 0.12) !important;
}
& .bg-red-500\/30 {
background-color: oklch(0.55 0.22 25 / 0.15) !important;
}
/* Input/textarea backgrounds */
& textarea,
& input[type="text"] {
color: var(--foreground) !important;
}
& textarea::placeholder,
& input::placeholder {
color: oklch(0.45 0 0 / 0.5) !important;
}
/* KaTeX / Math colors in light mode */
& .katex,
& .katex .mord,
& .katex .minner,
& .katex .mop,
& .katex .mbin,
& .katex .mrel,
& .katex .mpunct {
color: oklch(0.15 0 0) !important;
}
& .katex .frac-line,
& .katex .overline-line,
& .katex .underline-line,
& .katex .hline,
& .katex .rule {
border-color: oklch(0.25 0 0) !important;
background: oklch(0.25 0 0) !important;
}
& .katex .sqrt-line {
border-color: oklch(0.25 0 0) !important;
}
& .katex svg {
fill: oklch(0.25 0 0) !important;
stroke: oklch(0.25 0 0) !important;
}
& .katex svg path {
stroke: oklch(0.25 0 0) !important;
}
& .katex .delimsizing,
& .katex .delim-size1,
& .katex .delim-size2,
& .katex .delim-size3,
& .katex .delim-size4,
& .katex .mopen,
& .katex .mclose {
color: oklch(0.35 0 0) !important;
}
/* Code blocks in light mode */
& .hljs {
color: oklch(0.25 0 0) !important;
}
& .code-block-wrapper,
& .math-display-wrapper {
background: oklch(0.96 0 0) !important;
border-color: oklch(0.85 0 0) !important;
}
& .code-block-header,
& .math-display-header {
background: oklch(0.93 0 0) !important;
border-color: oklch(0.87 0 0) !important;
}
/* Inline code */
& .inline-code {
background: oklch(0.90 0 0) !important;
color: oklch(0.25 0 0) !important;
}
/* Blockquote */
& blockquote {
background: oklch(0.94 0 0) !important;
}
/* Table */
& th {
background: oklch(0.92 0 0) !important;
border-color: oklch(0.82 0 0) !important;
}
& td {
border-color: oklch(0.85 0 0) !important;
}
& hr {
border-color: oklch(0.85 0 0) !important;
}
/* hljs syntax colors for light mode */
& .hljs-keyword,
& .hljs-selector-tag,
& .hljs-literal,
& .hljs-section,
& .hljs-link {
color: oklch(0.50 0.25 300) !important;
}
& .hljs-string,
& .hljs-title,
& .hljs-name,
& .hljs-type,
& .hljs-attribute,
& .hljs-symbol,
& .hljs-bullet,
& .hljs-addition,
& .hljs-variable,
& .hljs-template-tag,
& .hljs-template-variable {
color: oklch(0.45 0.12 55) !important;
}
& .hljs-comment,
& .hljs-quote,
& .hljs-deletion,
& .hljs-meta {
color: oklch(0.55 0 0) !important;
}
& .hljs-number,
& .hljs-regexp,
& .hljs-built_in {
color: oklch(0.45 0.16 160) !important;
}
& .hljs-function,
& .hljs-class .hljs-title {
color: oklch(0.45 0.18 250) !important;
}
/* LaTeX proof/theorem blocks */
& .latex-proof {
background: oklch(0.97 0 0) !important;
border-left-color: oklch(0.70 0 0) !important;
}
& .latex-proof-header {
color: oklch(0.25 0 0) !important;
}
& .latex-proof-content {
color: oklch(0.15 0 0) !important;
}
& .latex-proof-content::after {
color: oklch(0.45 0 0) !important;
}
& .latex-theorem {
background: oklch(0.95 0 0) !important;
border-color: oklch(0.82 0 0) !important;
}
& .latex-diagram-placeholder {
background: oklch(0.97 0 0) !important;
border-color: oklch(0.82 0 0) !important;
color: oklch(0.35 0 0) !important;
}
}
/* Shooting Stars Animation */
.shooting-stars {
position: fixed;
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
@@ -39,7 +39,7 @@
<div class="flex flex-wrap gap-2 mb-3 px-1">
{#each files as file (file.id)}
<div
class="group relative flex items-center gap-2 bg-exo-dark-gray/80 border border-exo-yellow/30 rounded px-2.5 py-1.5 text-xs font-mono transition-all hover:border-exo-yellow/50 hover:shadow-[0_0_10px_rgba(255,215,0,0.1)]"
class="group relative flex items-center gap-2 bg-exo-dark-gray/80 border border-exo-yellow/30 rounded px-2.5 py-1.5 text-xs font-mono transition-all hover:border-exo-yellow/50 hover:shadow-[0_0_10px_var(--exo-glow-yellow)]"
>
<!-- File preview or icon -->
{#if file.preview && getFileCategory(file.type, file.name) === "image"}
+2 -2
View File
@@ -103,7 +103,7 @@
const modelSupportsThinking = $derived(() => {
if (!currentModel) return false;
const caps = modelCapabilities[currentModel] || [];
return caps.includes("thinking_toggle") && caps.includes("text");
return caps.includes("thinking") && caps.includes("text");
});
const isEditOnlyWithoutImage = $derived(
@@ -659,7 +659,7 @@
class="px-2.5 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-xs tracking-[0.1em] sm:tracking-[0.15em] uppercase font-medium transition-all duration-200 whitespace-nowrap
{!canSend || loading || isEditOnlyWithoutImage
? 'bg-exo-medium-gray/50 text-exo-light-gray cursor-not-allowed'
: 'bg-exo-yellow text-exo-black hover:bg-exo-yellow-darker hover:shadow-[0_0_20px_rgba(255,215,0,0.3)]'}"
: 'bg-exo-yellow text-exo-black hover:bg-exo-yellow-darker hover:shadow-[0_0_20px_var(--exo-glow-yellow)]'}"
aria-label={shouldShowEditMode
? "Edit image"
: isImageModel()
@@ -271,7 +271,7 @@
<!-- Assistant message header -->
<div class="flex items-center gap-1.5 sm:gap-2 mb-1.5 sm:mb-2">
<div
class="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-exo-yellow rounded-full shadow-[0_0_10px_rgba(255,215,0,0.5)]"
class="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-exo-yellow rounded-full shadow-[0_0_10px_var(--exo-glow-yellow)]"
></div>
<span
class="text-sm sm:text-xs text-exo-yellow tracking-[0.15em] sm:tracking-[0.2em] uppercase font-medium"
+62 -7
View File
@@ -1,11 +1,22 @@
<script lang="ts">
import { browser } from "$app/environment";
import { toggleMode, mode } from "mode-watcher";
export let showHome = true;
export let onHome: (() => void) | null = null;
export let showSidebarToggle = false;
export let sidebarVisible = true;
export let onToggleSidebar: (() => void) | null = null;
let {
showHome = true,
onHome = null,
showSidebarToggle = false,
sidebarVisible = true,
onToggleSidebar = null,
}: {
showHome?: boolean;
onHome?: (() => void) | null;
showSidebarToggle?: boolean;
sidebarVisible?: boolean;
onToggleSidebar?: (() => void) | null;
} = $props();
const currentMode = $derived(mode.current);
function handleHome(): void {
if (onHome) {
@@ -75,14 +86,58 @@
<img
src="/exo-logo.png"
alt="EXO"
class="h-18 drop-shadow-[0_0_20px_rgba(255,215,0,0.5)]"
class="h-18 drop-shadow-[0_0_20px_color-mix(in_oklch,var(--exo-yellow)_50%,transparent)]"
/>
</button>
<!-- Right: Home + Downloads -->
<!-- Right: Theme Toggle + Home + Downloads -->
<div
class="absolute right-6 top-1/2 -translate-y-1/2 flex items-center gap-4"
>
<!-- Theme Toggle Button -->
<button
onclick={() => toggleMode()}
class="p-2 rounded border border-exo-medium-gray/40 hover:border-exo-yellow/50 transition-colors cursor-pointer"
title={currentMode === "dark"
? "Switch to light mode"
: "Switch to dark mode"}
aria-label={currentMode === "dark"
? "Switch to light mode"
: "Switch to dark mode"}
>
{#if currentMode === "dark"}
<!-- Sun icon - click to go light -->
<svg
class="w-4 h-4 text-exo-light-gray hover:text-exo-yellow transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5" />
<path
stroke-linecap="round"
d="M12 1v2m0 18v2m-9-11H1m22 0h-2m-2.636-6.364l-1.414 1.414M6.05 6.05L4.636 4.636m0 14.728l1.414-1.414m11.314 1.414l1.414 1.414"
/>
</svg>
{:else}
<!-- Moon icon - click to go dark -->
<svg
class="w-4 h-4 text-exo-light-gray hover:text-exo-yellow transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"
/>
</svg>
{/if}
</button>
{#if showHome}
<button
onclick={handleHome}
@@ -59,14 +59,13 @@
}
const sizeOptions: ImageGenerationParams["size"][] = [
"auto",
"512x512",
"768x768",
"1024x1024",
"1024x768",
"768x1024",
"1024x1536",
"1536x1024",
"1024x1365",
"1365x1024",
];
const qualityOptions: ImageGenerationParams["quality"][] = [
@@ -177,90 +176,92 @@
<div class="border-b border-exo-medium-gray/30 px-3 py-2">
<!-- Basic params row -->
<div class="flex items-center gap-3 flex-wrap">
<!-- Size -->
<div class="flex items-center gap-1.5">
<span class="text-xs text-exo-light-gray uppercase tracking-wider"
>SIZE:</span
>
<div class="relative">
<button
bind:this={sizeButtonRef}
type="button"
onclick={() => (isSizeDropdownOpen = !isSizeDropdownOpen)}
class="bg-exo-medium-gray/50 border border-exo-yellow/30 rounded pl-2 pr-6 py-1 text-xs font-mono text-exo-yellow cursor-pointer transition-all duration-200 hover:border-exo-yellow/50 focus:outline-none focus:border-exo-yellow/70 {isSizeDropdownOpen
? 'border-exo-yellow/70'
: ''}"
<!-- Size (hidden in edit mode - output size comes from input image) -->
{#if !isEditMode}
<div class="flex items-center gap-1.5">
<span class="text-xs text-exo-light-gray uppercase tracking-wider"
>SIZE:</span
>
{params.size.toUpperCase()}
</button>
<div
class="absolute right-1.5 top-1/2 -translate-y-1/2 pointer-events-none transition-transform duration-200 {isSizeDropdownOpen
? 'rotate-180'
: ''}"
>
<svg
class="w-3 h-3 text-exo-yellow/60"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
<div class="relative">
<button
bind:this={sizeButtonRef}
type="button"
onclick={() => (isSizeDropdownOpen = !isSizeDropdownOpen)}
class="bg-exo-medium-gray/50 border border-exo-yellow/30 rounded pl-2 pr-6 py-1 text-xs font-mono text-exo-yellow cursor-pointer transition-all duration-200 hover:border-exo-yellow/50 focus:outline-none focus:border-exo-yellow/70 {isSizeDropdownOpen
? 'border-exo-yellow/70'
: ''}"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
{#if isSizeDropdownOpen}
<!-- Backdrop to close dropdown -->
<button
type="button"
class="fixed inset-0 z-[9998] cursor-default"
onclick={() => (isSizeDropdownOpen = false)}
aria-label="Close dropdown"
></button>
<!-- Dropdown Panel - fixed positioning to escape overflow:hidden -->
<div
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto overflow-x-hidden min-w-max"
style="bottom: calc(100vh - {sizeDropdownPosition()
.top}px + 4px); left: {sizeDropdownPosition().left}px;"
>
<div class="py-1">
{#each sizeOptions as size}
<button
type="button"
onclick={() => selectSize(size)}
class="w-full px-3 py-1.5 text-left text-xs font-mono tracking-wide transition-colors duration-100 flex items-center gap-2 {params.size ===
size
? 'bg-transparent text-exo-yellow'
: 'text-exo-light-gray hover:text-exo-yellow'}"
>
{#if params.size === size}
<svg
class="w-3 h-3 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
{:else}
<span class="w-3"></span>
{/if}
<span>{size.toUpperCase()}</span>
</button>
{/each}
{params.size}
</button>
<div
class="absolute right-1.5 top-1/2 -translate-y-1/2 pointer-events-none transition-transform duration-200 {isSizeDropdownOpen
? 'rotate-180'
: ''}"
>
<svg
class="w-3 h-3 text-exo-yellow/60"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
{/if}
</div>
{#if isSizeDropdownOpen}
<!-- Backdrop to close dropdown -->
<button
type="button"
class="fixed inset-0 z-[9998] cursor-default"
onclick={() => (isSizeDropdownOpen = false)}
aria-label="Close dropdown"
></button>
<!-- Dropdown Panel - fixed positioning to escape overflow:hidden -->
<div
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto min-w-max"
style="bottom: calc(100vh - {sizeDropdownPosition()
.top}px + 4px); left: {sizeDropdownPosition().left}px;"
>
<div class="py-1">
{#each sizeOptions as size}
<button
type="button"
onclick={() => selectSize(size)}
class="w-full px-3 py-1.5 text-left text-xs font-mono tracking-wide transition-colors duration-100 flex items-center gap-2 {params.size ===
size
? 'bg-transparent text-exo-yellow'
: 'text-exo-light-gray hover:text-exo-yellow'}"
>
{#if params.size === size}
<svg
class="w-3 h-3 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
{:else}
<span class="w-3"></span>
{/if}
<span>{size}</span>
</button>
{/each}
</div>
</div>
{/if}
</div>
{/if}
<!-- Quality -->
<div class="flex items-center gap-1.5">
@@ -310,7 +311,7 @@
<!-- Dropdown Panel - fixed positioning to escape overflow:hidden -->
<div
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto overflow-x-hidden min-w-max"
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto min-w-max"
style="bottom: calc(100vh - {qualityDropdownPosition()
.top}px + 4px); left: {qualityDropdownPosition().left}px;"
>
@@ -668,7 +669,7 @@
width: 12px;
height: 12px;
border-radius: 50%;
background: #ffd700;
background: var(--exo-yellow);
cursor: pointer;
border: none;
}
@@ -677,7 +678,7 @@
width: 12px;
height: 12px;
border-radius: 50%;
background: #ffd700;
background: var(--exo-yellow);
cursor: pointer;
border: none;
}
@@ -1051,4 +1051,231 @@
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ============================================
LIGHT MODE OVERRIDES
All dark-mode-specific hardcoded colors are
overridden below for light backgrounds.
============================================ */
/* --- Inline code --- */
:global(html:not(.dark)) .markdown-content :global(.inline-code) {
background: rgba(0, 0, 0, 0.06);
color: var(--exo-yellow, #b8860b);
}
/* --- Blockquote --- */
:global(html:not(.dark)) .markdown-content :global(blockquote) {
background: rgba(0, 0, 0, 0.03);
}
/* --- Tables --- */
:global(html:not(.dark)) .markdown-content :global(th) {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.12);
}
:global(html:not(.dark)) .markdown-content :global(td) {
border: 1px solid rgba(0, 0, 0, 0.1);
}
/* --- Horizontal rule --- */
:global(html:not(.dark)) .markdown-content :global(hr) {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
/* --- Code block wrapper --- */
:global(html:not(.dark)) .markdown-content :global(.code-block-wrapper) {
border: 1px solid rgba(0, 0, 0, 0.12);
background: rgba(0, 0, 0, 0.03);
}
:global(html:not(.dark)) .markdown-content :global(.code-block-header) {
background: rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
/* --- Syntax highlighting - light theme --- */
:global(html:not(.dark)) .markdown-content :global(.hljs) {
color: #1f2937;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-keyword),
:global(html:not(.dark)) .markdown-content :global(.hljs-selector-tag),
:global(html:not(.dark)) .markdown-content :global(.hljs-literal),
:global(html:not(.dark)) .markdown-content :global(.hljs-section),
:global(html:not(.dark)) .markdown-content :global(.hljs-link) {
color: #7c3aed;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-string),
:global(html:not(.dark)) .markdown-content :global(.hljs-title),
:global(html:not(.dark)) .markdown-content :global(.hljs-name),
:global(html:not(.dark)) .markdown-content :global(.hljs-type),
:global(html:not(.dark)) .markdown-content :global(.hljs-attribute),
:global(html:not(.dark)) .markdown-content :global(.hljs-symbol),
:global(html:not(.dark)) .markdown-content :global(.hljs-bullet),
:global(html:not(.dark)) .markdown-content :global(.hljs-addition),
:global(html:not(.dark)) .markdown-content :global(.hljs-variable),
:global(html:not(.dark)) .markdown-content :global(.hljs-template-tag),
:global(html:not(.dark)) .markdown-content :global(.hljs-template-variable) {
color: #b45309;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-comment),
:global(html:not(.dark)) .markdown-content :global(.hljs-quote),
:global(html:not(.dark)) .markdown-content :global(.hljs-deletion),
:global(html:not(.dark)) .markdown-content :global(.hljs-meta) {
color: #9ca3af;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-number),
:global(html:not(.dark)) .markdown-content :global(.hljs-regexp),
:global(html:not(.dark)) .markdown-content :global(.hljs-literal),
:global(html:not(.dark)) .markdown-content :global(.hljs-built_in) {
color: #059669;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-function),
:global(html:not(.dark)) .markdown-content :global(.hljs-class .hljs-title) {
color: #2563eb;
}
/* --- KaTeX math - light mode --- */
:global(html:not(.dark)) .markdown-content :global(.katex) {
color: oklch(0.2 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .mord),
:global(html:not(.dark)) .markdown-content :global(.katex .minner),
:global(html:not(.dark)) .markdown-content :global(.katex .mop),
:global(html:not(.dark)) .markdown-content :global(.katex .mbin),
:global(html:not(.dark)) .markdown-content :global(.katex .mrel),
:global(html:not(.dark)) .markdown-content :global(.katex .mpunct) {
color: oklch(0.2 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .frac-line),
:global(html:not(.dark)) .markdown-content :global(.katex .overline-line),
:global(html:not(.dark)) .markdown-content :global(.katex .underline-line),
:global(html:not(.dark)) .markdown-content :global(.katex .hline),
:global(html:not(.dark)) .markdown-content :global(.katex .rule) {
border-color: oklch(0.25 0 0) !important;
background: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .sqrt-line) {
border-color: oklch(0.25 0 0) !important;
}
:global(html:not(.dark)) .markdown-content :global(.katex svg) {
fill: oklch(0.25 0 0);
stroke: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex svg path) {
stroke: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .delimsizing),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size1),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size2),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size3),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size4),
:global(html:not(.dark)) .markdown-content :global(.katex .mopen),
:global(html:not(.dark)) .markdown-content :global(.katex .mclose) {
color: oklch(0.35 0 0);
}
/* --- Display math wrapper --- */
:global(html:not(.dark)) .markdown-content :global(.math-display-wrapper) {
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.02);
}
:global(html:not(.dark))
.markdown-content
:global(.math-display-wrapper:hover) {
border-color: rgba(0, 0, 0, 0.18);
box-shadow: 0 0 12px rgba(0, 0, 0, 0.05);
}
:global(html:not(.dark)) .markdown-content :global(.math-display-header) {
background: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
:global(html:not(.dark)) .markdown-content :global(.math-label) {
color: rgba(0, 0, 0, 0.5);
}
/* --- Math scrollbar (light mode) --- */
:global(html:not(.dark))
.markdown-content
:global(.math-display-content::-webkit-scrollbar-track) {
background: rgba(0, 0, 0, 0.04);
}
:global(html:not(.dark))
.markdown-content
:global(.math-display-content::-webkit-scrollbar-thumb) {
background: rgba(0, 0, 0, 0.15);
}
:global(html:not(.dark))
.markdown-content
:global(.math-display-content::-webkit-scrollbar-thumb:hover) {
background: rgba(0, 0, 0, 0.25);
}
/* --- Inline math hover --- */
:global(html:not(.dark)) .markdown-content :global(.math-inline:hover) {
background: rgba(0, 0, 0, 0.04);
}
/* --- LaTeX proof environment --- */
:global(html:not(.dark)) .markdown-content :global(.latex-proof) {
background: rgba(0, 0, 0, 0.02);
border-left: 3px solid rgba(0, 0, 0, 0.25);
}
:global(html:not(.dark)) .markdown-content :global(.latex-proof-header) {
color: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.latex-proof-content) {
color: oklch(0.2 0 0);
}
:global(html:not(.dark))
.markdown-content
:global(.latex-proof-content::after) {
color: oklch(0.4 0 0);
}
/* --- LaTeX theorem-like environments --- */
:global(html:not(.dark)) .markdown-content :global(.latex-theorem) {
background: rgba(0, 0, 0, 0.02);
border: 1px solid rgba(0, 0, 0, 0.1);
}
:global(html:not(.dark)) .markdown-content :global(.latex-theorem-content) {
color: oklch(0.2 0 0);
}
/* --- LaTeX diagram/figure placeholder --- */
:global(html:not(.dark))
.markdown-content
:global(.latex-diagram-placeholder) {
background: rgba(0, 0, 0, 0.02);
border: 1px dashed rgba(0, 0, 0, 0.2);
color: rgba(0, 0, 0, 0.45);
}
/* --- Math error (light mode) --- */
:global(html:not(.dark)) .markdown-content :global(.math-error) {
color: #dc2626;
background: rgba(220, 38, 38, 0.08);
border: 1px solid rgba(220, 38, 38, 0.15);
}
</style>
+64 -33
View File
@@ -6,6 +6,42 @@
TopologyEdge,
} from "$lib/stores/app.svelte";
import { debugMode, topologyData } from "$lib/stores/app.svelte";
import { browser } from "$app/environment";
// Theme detection
let isDark = $state(true);
$effect(() => {
if (!browser) return;
const update = () => {
isDark = document.documentElement.classList.contains("dark");
};
update();
const observer = new MutationObserver(update);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
});
// Theme-aware colors
let tc = $derived({
accent: isDark ? "#FFD700" : "#1a1a1a",
accentDim: isDark ? "rgba(255,215,0,0.1)" : "rgba(26,26,26,0.06)",
accentGlow: isDark ? "rgba(255,215,0,0.1)" : "rgba(0,0,0,0.03)",
outlineActive: isDark ? "#FFD700" : "#374151",
outlineInactive: isDark ? "#4B5563" : "#d1d5db",
strokeActive: isDark ? "#FFD700" : "#374151",
strokeInactive: isDark ? "#4B5563" : "#d1d5db",
caseFill: isDark ? "#0a0a0a" : "#f3f4f6",
caseDetail: isDark ? "#374151" : "#d1d5db",
errorDot: isDark ? "#f87171" : "#dc2626",
statusInactive: isDark ? "#4B5563" : "#9ca3af",
connGood: isDark ? "rgba(255,255,255,0.85)" : "rgba(30,30,30,0.85)",
connBad: isDark ? "rgba(248,113,113,0.85)" : "rgba(220,38,38,0.85)",
scanlineColor: isDark ? "rgba(255,215,0,0.02)" : "rgba(0,0,0,0.01)",
});
interface Props {
model: { id: string; name?: string; storage_size_megabytes?: number };
@@ -491,7 +527,7 @@
<div
class="bg-exo-dark-gray/60 border {canFit
? 'border-exo-yellow/20 group-hover:border-exo-yellow/40'
: 'border-red-500/20'} p-3 transition-all duration-200 group-hover:shadow-[0_0_15px_rgba(255,215,0,0.1)]"
: 'border-red-500/20'} p-3 transition-all duration-200 group-hover:shadow-[0_0_15px_var(--exo-glow-yellow)]"
>
<!-- Model Name & Memory Required -->
<div class="flex items-start justify-between gap-2 mb-2">
@@ -589,7 +625,8 @@
>
<!-- Scanline effect -->
<div
class="absolute inset-0 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(255,215,0,0.02)_2px,rgba(255,215,0,0.02)_4px)] pointer-events-none"
style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, {tc.scanlineColor} 2px, {tc.scanlineColor} 4px)"
class="absolute inset-0 pointer-events-none"
></div>
<svg
@@ -670,7 +707,9 @@
y1={node.y}
x2={node2.x}
y2={node2.y}
stroke={node.isUsed && node2.isUsed ? "#FFD700" : "#374151"}
stroke={node.isUsed && node2.isUsed
? tc.strokeActive
: tc.strokeInactive}
stroke-width="1"
stroke-dasharray={node.isUsed && node2.isUsed ? "4,2" : "2,4"}
opacity={node.isUsed && node2.isUsed ? 0.4 : 0.15}
@@ -706,9 +745,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
@@ -725,9 +762,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 +781,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 +800,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 +832,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"
/>
<!-- Screen area (memory fill container) -->
@@ -810,7 +841,7 @@
y="2"
width={node.iconSize - 8}
height={node.screenHeight}
fill="#0a0a0a"
fill={tc.caseFill}
/>
<!-- Current memory fill (gray) -->
<rect
@@ -818,7 +849,7 @@
y={2 + node.screenHeight - node.currentFillHeight}
width={node.iconSize - 8}
height={node.currentFillHeight}
fill="#374151"
fill={tc.caseDetail}
/>
<!-- New model memory fill (glowing yellow) -->
{#if node.modelUsageGB > 0 && node.isUsed}
@@ -830,7 +861,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 +873,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"
/>
</g>
@@ -859,7 +890,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"
/>
<!-- Memory fill background -->
@@ -868,7 +899,7 @@
y="4"
width={node.iconSize - 8}
height={node.iconSize - 8}
fill="#0a0a0a"
fill={tc.caseFill}
/>
<!-- Current memory fill -->
<rect
@@ -877,7 +908,7 @@
(node.iconSize - 8) * (1 - node.currentPercent / 100)}
width={node.iconSize - 8}
height={(node.iconSize - 8) * (node.currentPercent / 100)}
fill="#374151"
fill={tc.caseDetail}
/>
<!-- New model memory fill -->
{#if node.modelUsageGB > 0 && node.isUsed}
@@ -887,7 +918,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 +937,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"
/>
<!-- Memory fill background -->
@@ -915,7 +946,7 @@
y={node.iconSize * 0.32}
width={node.iconSize - 8}
height={node.iconSize * 0.36}
fill="#0a0a0a"
fill={tc.caseFill}
/>
<!-- Current memory fill -->
<rect
@@ -924,7 +955,7 @@
node.iconSize * 0.36 * (1 - node.currentPercent / 100)}
width={node.iconSize - 8}
height={node.iconSize * 0.36 * (node.currentPercent / 100)}
fill="#374151"
fill={tc.caseDetail}
/>
<!-- New model memory fill -->
{#if node.modelUsageGB > 0 && node.isUsed}
@@ -936,7 +967,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 +986,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"
/>
</g>
@@ -970,9 +1001,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)}%
</text>
+127 -48
View File
@@ -36,6 +36,84 @@
const rdmaCtlData = $derived(nodeRdmaCtl());
const identitiesData = $derived(nodeIdentities());
// Theme-aware colors - read from CSS custom properties
function getThemeColors() {
if (typeof document === "undefined") return getDefaultColors();
const style = getComputedStyle(document.documentElement);
const isDark = document.documentElement.classList.contains("dark");
return {
accent:
style.getPropertyValue("--exo-yellow").trim() ||
(isDark ? "oklch(0.85 0.18 85)" : "oklch(0.20 0.02 85)"),
accentRgb: isDark ? "255,215,0" : "30,28,20",
deviceCase: isDark ? "#1a1a1a" : "#e8e8e8",
deviceCaseDark: isDark ? "#2c2c2c" : "#d4d4d4",
deviceScreen: isDark ? "#0a0a12" : "#f0f0f5",
deviceScreenFill: isDark ? "rgba(0,20,40,0.9)" : "rgba(240,242,248,0.95)",
labelWhite: isDark ? "#FFFFFF" : "#1a1a1a",
labelMuted: isDark ? "rgba(179,179,179,0.9)" : "rgba(80,80,80,0.9)",
labelDim: isDark ? "rgba(179,179,179,0.7)" : "rgba(100,100,100,0.7)",
wireDefault: isDark ? "rgba(179,179,179,0.8)" : "rgba(120,120,120,0.6)",
wireBright: isDark ? "rgba(255,255,255,0.9)" : "rgba(30,30,30,0.9)",
wireFiltered: isDark ? "rgba(140,140,140,0.6)" : "rgba(160,160,160,0.5)",
gridStroke: isDark
? "var(--exo-light-gray, #B3B3B3)"
: "var(--exo-light-gray, #888888)",
errorText: isDark ? "rgba(248,113,113,0.9)" : "rgba(220,38,38,0.9)",
normalText: isDark ? "rgba(255,255,255,0.85)" : "rgba(30,30,30,0.85)",
gpuChip: isDark ? "rgba(80, 80, 90, 0.7)" : "rgba(180, 180, 190, 0.7)",
detailOverlay: isDark ? "rgba(0,0,0,0.35)" : "rgba(0,0,0,0.08)",
deviceShadow: isDark ? "rgba(0,0,0,0.2)" : "rgba(0,0,0,0.06)",
deviceHighlight: isDark
? "rgba(255,255,255,0.08)"
: "rgba(255,255,255,0.5)",
tbActive: isDark ? "rgba(234,179,8,0.9)" : "rgba(30,28,20,0.9)",
tbInactive: isDark ? "rgba(100,100,100,0.7)" : "rgba(160,160,160,0.7)",
deviceDetail: isDark ? "#374151" : "#c0c0c0",
};
}
function getDefaultColors() {
return {
accent: "oklch(0.85 0.18 85)",
accentRgb: "255,215,0",
deviceCase: "#1a1a1a",
deviceCaseDark: "#2c2c2c",
deviceScreen: "#0a0a12",
deviceScreenFill: "rgba(0,20,40,0.9)",
labelWhite: "#FFFFFF",
labelMuted: "rgba(179,179,179,0.9)",
labelDim: "rgba(179,179,179,0.7)",
wireDefault: "rgba(179,179,179,0.8)",
wireBright: "rgba(255,255,255,0.9)",
wireFiltered: "rgba(140,140,140,0.6)",
gridStroke: "var(--exo-light-gray, #B3B3B3)",
errorText: "rgba(248,113,113,0.9)",
normalText: "rgba(255,255,255,0.85)",
gpuChip: "rgba(80, 80, 90, 0.7)",
detailOverlay: "rgba(0,0,0,0.35)",
deviceShadow: "rgba(0,0,0,0.2)",
deviceHighlight: "rgba(255,255,255,0.08)",
tbActive: "rgba(234,179,8,0.9)",
tbInactive: "rgba(100,100,100,0.7)",
deviceDetail: "#374151",
};
}
let themeColors = $state(getThemeColors());
$effect(() => {
if (typeof document === "undefined") return;
const observer = new MutationObserver(() => {
themeColors = getThemeColors();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
});
function getNodeLabel(nodeId: string): string {
const node = data?.nodes?.[nodeId];
return node?.friendly_name || nodeId.slice(0, 8);
@@ -127,7 +205,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 +286,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 +299,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 +583,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;
@@ -565,22 +643,22 @@
// Holographic wireframe colors - bright yellow for filter, subtle yellow for hover, grey for filtered out
const wireColor = isInFilter
? "rgba(255,215,0,1)" // Bright yellow for filter selection
? `rgba(${themeColors.accentRgb},1)` // Bright yellow for filter selection
: isHovered
? "rgba(255,215,0,0.7)" // Subtle yellow for hover
? `rgba(${themeColors.accentRgb},0.7)` // Subtle yellow for hover
: isHighlighted
? "rgba(255,215,0,0.9)" // Yellow for instance highlight
? `rgba(${themeColors.accentRgb},0.9)` // Yellow for instance highlight
: 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 // Grey for filtered out
: themeColors.wireDefault; // Default
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 +666,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 +731,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 +749,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 +814,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 +832,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 +892,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 +904,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 +920,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 +937,7 @@
"transform",
`translate(${logoX}, ${logoY}) scale(${logoScale})`,
)
.attr("fill", "#FFFFFF")
.attr("fill", themeColors.labelWhite)
.attr("opacity", 0.9);
// Base (keyboard) - trapezoidal
@@ -875,7 +953,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 +968,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 +982,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 +1020,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 +1057,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 +1070,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 +1083,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 +1111,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 +1128,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 +1153,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 +1165,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 +1186,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 +1203,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 +1227,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 +1246,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 +1267,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 +1284,7 @@
const _hoveredNodeId = hoveredNodeId;
const _filteredNodes = filteredNodes;
const _highlightedNodes = highlightedNodes;
const _themeColors = themeColors;
if (_data) {
renderGraph();
}
+3
View File
@@ -1,9 +1,12 @@
<script lang="ts">
import "../app.css";
import { ModeWatcher } from "mode-watcher";
let { children } = $props();
</script>
<ModeWatcher defaultMode="dark" />
<svelte:head>
<title>EXO</title>
<meta name="description" content="EXO - Distributed AI Cluster Dashboard" />
+3 -3
View File
@@ -2299,7 +2299,7 @@
<!-- Panel Header -->
<div class="flex items-center gap-2 mb-4">
<div
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_var(--exo-glow-yellow)] animate-pulse"
></div>
<h3
class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
@@ -2948,7 +2948,7 @@
<!-- Dot -->
<span
class="rounded-full transition-all {isSelected
? 'w-6 h-6 bg-exo-yellow shadow-[0_0_10px_rgba(255,215,0,0.6)]'
? 'w-6 h-6 bg-exo-yellow shadow-[0_0_10px_var(--exo-glow-yellow)]'
: isValid
? 'w-4 h-4 bg-exo-light-gray/70 mt-1'
: 'w-3 h-3 bg-exo-medium-gray/50 mt-1.5'}"
@@ -3114,7 +3114,7 @@
<!-- Panel Header -->
<div class="flex items-center gap-2 mb-4">
<div
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_var(--exo-glow-yellow)] animate-pulse"
></div>
<h3
class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
+1 -1
View File
@@ -382,7 +382,7 @@
class="group border-b border-exo-medium-gray/20 hover:bg-exo-medium-gray/10 transition-colors"
>
<td
class="sticky left-0 z-10 bg-exo-dark-gray group-hover:bg-[oklch(0.18_0_0)] transition-colors px-4 py-3 whitespace-nowrap border-r border-exo-medium-gray/20"
class="sticky left-0 z-10 bg-exo-dark-gray group-hover:bg-[var(--exo-bg-hover)] transition-colors px-4 py-3 whitespace-nowrap border-r border-exo-medium-gray/20"
>
<div class="flex items-center gap-2">
<div class="min-w-0">