commit 111fd13e8ec810c3ae1455b4a632bde9de285994
parent 7c654ac66b26f774aa6c918fed7e20e1ad3bf1c1
Author: Brian Graham <brian@buildingbetterteams.de>
Date: Sat, 4 Apr 2026 09:32:38 +0200
Align theme with SMUI, add light/dark mode toggle
Theme aligned with ~/smui design system:
- Raw HSL triplet variables for alpha support
- Full SMUI semantic tokens (background, foreground, card, primary, etc.)
- Frost palette (frost-1 through frost-4)
- Chart colors, kbd styling, alert variants, skeleton animation
Light mode:
- Snow Storm palette for light theme
- data-theme toggle saved to localStorage
- Sun/moon toggle button in header nav
- prefers-color-scheme media query fallback
Legacy aliases preserved for all existing components.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 392 insertions(+), 76 deletions(-)
diff --git a/dashboard/src/layouts/Base.astro b/dashboard/src/layouts/Base.astro
@@ -17,7 +17,7 @@ try {
---
<!doctype html>
-<html lang="en">
+<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -25,29 +25,63 @@ try {
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>{title} | Loop Benchmarking</title>
+ <script is:inline>
+ (function() {
+ var saved = localStorage.getItem('theme');
+ if (saved === 'light' || saved === 'dark') {
+ document.documentElement.setAttribute('data-theme', saved);
+ } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
+ document.documentElement.setAttribute('data-theme', 'light');
+ } else {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ }
+ })();
+ </script>
</head>
<body>
- <header style="border-bottom: 1px solid var(--border); padding: 16px 0; margin-bottom: 32px;">
+ <header style="border-bottom: 1px solid hsl(var(--border)); padding: 16px 0; margin-bottom: 32px;">
<div class="container" style="display: flex; align-items: center; gap: 24px;">
- <a href="/" style="font-weight: 700; font-size: 1.1rem; color: var(--text);">
+ <a href="/" style="font-weight: 700; font-size: 1.1rem; color: hsl(var(--foreground));">
Loop Benchmarking
</a>
- <nav style="display: flex; gap: 16px; font-size: 0.875rem;">
+ <nav style="display: flex; gap: 16px; font-size: 0.875rem; align-items: center;">
<a href="/">Grid</a>
<a href="/insights">Insights</a>
<a href="/explore">Explore</a>
<a href="/compare">Compare</a>
</nav>
+ <button
+ id="theme-toggle"
+ class="theme-toggle"
+ type="button"
+ aria-label="Toggle theme"
+ style="margin-left: auto;"
+ >
+ <svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
+ <svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
+ </button>
</div>
</header>
<main class="container">
<slot />
</main>
- <footer style="border-top: 1px solid var(--border); padding: 24px 0; margin-top: 48px;">
- <div class="container" style="text-align: center; color: var(--text-muted); font-size: 0.75rem;">
+ <footer style="border-top: 1px solid hsl(var(--border)); padding: 24px 0; margin-top: 48px;">
+ <div class="container" style="text-align: center; color: hsl(var(--muted-foreground)); font-size: 0.75rem;">
Loop Benchmarking - Open agentic loop benchmark data
<span style="margin-left: 12px; font-family: var(--font-mono);">{gitCommit}</span>
</div>
</footer>
+ <script is:inline>
+ (function() {
+ var btn = document.getElementById('theme-toggle');
+ if (!btn) return;
+ btn.addEventListener('click', function() {
+ var current = document.documentElement.getAttribute('data-theme');
+ var next = current === 'dark' ? 'light' : 'dark';
+ document.documentElement.setAttribute('data-theme', next);
+ localStorage.setItem('theme', next);
+ });
+ })();
+ </script>
</body>
</html>
diff --git a/dashboard/src/styles/global.css b/dashboard/src/styles/global.css
@@ -1,36 +1,65 @@
/* ============================================================
Loop Benchmarking - Ship the Loop Design System
+ Built on SMUI (SpaceMolt UI) -- terminal-aesthetic, Nord-inspired
Font: JetBrains Mono (monospace everywhere)
- Palette: Nord-inspired HSL with aurora accents
+ Modes: dark (default), light (Snow Storm)
============================================================ */
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap");
+/* ============================================================
+ Dark mode (default)
+ ============================================================ */
+
:root {
- /* Surfaces */
- --surface-0: hsl(213 16% 12%);
- --surface-1: hsl(217 16% 15.5%);
- --surface-2: hsl(216 15% 19%);
- --surface-3: hsl(215 14% 22%);
-
- /* Text */
- --text: hsl(213 27% 88%);
- --text-muted: hsl(213 14% 65%);
-
- /* Borders */
- --border: hsl(217 17% 28%);
- --border-hover: hsl(217 17% 36%);
-
- /* Accent / frost */
- --accent: hsl(193 44% 67%);
- --accent-hover: hsl(193 50% 75%);
-
- /* Aurora */
- --green: hsl(92 28% 65%);
- --yellow: hsl(40 71% 73%);
- --orange: hsl(14 51% 63%);
- --red: hsl(355 52% 64%);
- --purple: hsl(311 24% 63%);
+ /* shadcn/ui base variables (raw HSL triplets for alpha support) */
+ --background: 213 16% 12%;
+ --foreground: 213 27% 88%;
+ --card: 217 16% 15.5%;
+ --card-foreground: 213 27% 88%;
+ --popover: 217 16% 15.5%;
+ --popover-foreground: 213 27% 88%;
+ --primary: 193 44% 67%;
+ --primary-foreground: 213 16% 12%;
+ --secondary: 216 15% 19%;
+ --secondary-foreground: 213 27% 88%;
+ --muted: 216 15% 19%;
+ --muted-foreground: 213 14% 65%;
+ --destructive: 355 52% 65%;
+ --destructive-foreground: 219 28% 94%;
+ --border: 217 17% 28%;
+ --input: 217 17% 28%;
+ --ring: 193 44% 67%;
+ --radius: 0rem;
+
+ /* Chart colors */
+ --chart-1: 193 44% 67%;
+ --chart-2: 213 32% 52%;
+ --chart-3: 92 28% 65%;
+ --chart-4: 40 71% 73%;
+ --chart-5: 311 24% 63%;
+
+ /* Frost blues */
+ --smui-frost-1: 176 25% 65%;
+ --smui-frost-2: 193 44% 67%;
+ --smui-frost-3: 210 34% 63%;
+ --smui-frost-4: 213 32% 52%;
+
+ /* Aurora status colors */
+ --smui-green: 92 28% 65%;
+ --smui-yellow: 40 71% 73%;
+ --smui-orange: 14 51% 63%;
+ --smui-red: 355 52% 64%;
+ --smui-purple: 311 24% 63%;
+
+ /* Surface hierarchy (Polar Night) */
+ --smui-surface-0: 213 16% 12%;
+ --smui-surface-1: 217 16% 15.5%;
+ --smui-surface-2: 216 15% 19%;
+ --smui-surface-3: 215 14% 22%;
+
+ /* Interactive */
+ --smui-border-hover: 216 12% 37%;
/* Typography */
--font-mono: "JetBrains Mono", monospace;
@@ -38,13 +67,127 @@
--text-ui: 13px;
--text-heading: 22px;
--text-stat: 26px;
-
- /* Legacy aliases so inline styles in Base.astro keep working */
+ --text-hero: 42px;
+
+ /* Legacy aliases so existing components keep working.
+ Components use var(--accent) to mean "frost blue text color",
+ but SMUI uses --accent for a different semantic role.
+ These resolved-color aliases bridge the gap. */
+ --surface-0: hsl(var(--smui-surface-0));
+ --surface-1: hsl(var(--smui-surface-1));
+ --surface-2: hsl(var(--smui-surface-2));
+ --surface-3: hsl(var(--smui-surface-3));
+ --text: hsl(var(--foreground));
+ --text-muted: hsl(var(--muted-foreground));
+ --border-hover: hsl(var(--smui-border-hover));
+ /* --accent in legacy components means the frost blue accent color */
+ --accent: hsl(var(--primary));
+ --accent-hover: hsl(var(--primary) / 0.8);
+ --green: hsl(var(--smui-green));
+ --yellow: hsl(var(--smui-yellow));
+ --orange: hsl(var(--smui-orange));
+ --red: hsl(var(--smui-red));
+ --purple: hsl(var(--smui-purple));
--bg: var(--surface-0);
--bg-card: var(--surface-1);
--bg-hover: var(--surface-2);
}
+/* ============================================================
+ Light mode (Snow Storm)
+ ============================================================ */
+
+[data-theme="light"] {
+ --background: 218 27% 94%;
+ --foreground: 220 16% 22%;
+ --card: 218 27% 89%;
+ --card-foreground: 220 16% 22%;
+ --popover: 218 27% 89%;
+ --popover-foreground: 220 16% 22%;
+ --primary: 213 32% 40%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 219 28% 84%;
+ --secondary-foreground: 220 16% 22%;
+ --muted: 219 28% 84%;
+ --muted-foreground: 220 17% 28%;
+ --destructive: 355 60% 45%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 219 18% 72%;
+ --input: 219 18% 72%;
+ --ring: 213 32% 40%;
+
+ /* Chart colors (darkened for light backgrounds) */
+ --chart-1: 213 32% 44%;
+ --chart-2: 210 34% 42%;
+ --chart-3: 92 30% 36%;
+ --chart-4: 40 70% 35%;
+ --chart-5: 311 28% 42%;
+
+ /* Frost (darkened for light backgrounds) */
+ --smui-frost-1: 176 30% 35%;
+ --smui-frost-2: 193 40% 38%;
+ --smui-frost-3: 210 34% 40%;
+ --smui-frost-4: 213 32% 36%;
+
+ /* Aurora (darkened for light backgrounds) */
+ --smui-green: 92 35% 34%;
+ --smui-yellow: 40 70% 34%;
+ --smui-orange: 14 55% 44%;
+ --smui-red: 355 55% 44%;
+ --smui-purple: 311 28% 40%;
+
+ /* Surface hierarchy (Snow Storm) */
+ --smui-surface-0: 218 27% 94%;
+ --smui-surface-1: 218 27% 89%;
+ --smui-surface-2: 219 28% 84%;
+ --smui-surface-3: 219 20% 76%;
+
+ /* Interactive */
+ --smui-border-hover: 220 17% 50%;
+}
+
+/* Also support prefers-color-scheme for users without explicit toggle */
+@media (prefers-color-scheme: light) {
+ :root:not([data-theme="dark"]):not([data-theme="light"]) {
+ --background: 218 27% 94%;
+ --foreground: 220 16% 22%;
+ --card: 218 27% 89%;
+ --card-foreground: 220 16% 22%;
+ --popover: 218 27% 89%;
+ --popover-foreground: 220 16% 22%;
+ --primary: 213 32% 40%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 219 28% 84%;
+ --secondary-foreground: 220 16% 22%;
+ --muted: 219 28% 84%;
+ --muted-foreground: 220 17% 28%;
+ --destructive: 355 60% 45%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 219 18% 72%;
+ --input: 219 18% 72%;
+ --ring: 213 32% 40%;
+ --chart-1: 213 32% 44%;
+ --chart-2: 210 34% 42%;
+ --chart-3: 92 30% 36%;
+ --chart-4: 40 70% 35%;
+ --chart-5: 311 28% 42%;
+ --smui-frost-1: 176 30% 35%;
+ --smui-frost-2: 193 40% 38%;
+ --smui-frost-3: 210 34% 40%;
+ --smui-frost-4: 213 32% 36%;
+ --smui-green: 92 35% 34%;
+ --smui-yellow: 40 70% 34%;
+ --smui-orange: 14 55% 44%;
+ --smui-red: 355 55% 44%;
+ --smui-purple: 311 28% 40%;
+ --smui-surface-0: 218 27% 94%;
+ --smui-surface-1: 218 27% 89%;
+ --smui-surface-2: 219 28% 84%;
+ --smui-surface-3: 219 20% 76%;
+ --smui-border-hover: 220 17% 50%;
+ }
+}
+
/* ---- Reset ---- */
* {
@@ -53,12 +196,19 @@
box-sizing: border-box;
}
+/* ---- Selection ---- */
+
+::selection {
+ background: hsl(var(--primary) / 0.2);
+ color: hsl(var(--primary));
+}
+
/* ---- Base ---- */
body {
font-family: var(--font-mono);
- background: var(--surface-0);
- color: var(--text);
+ background: hsl(var(--background));
+ color: hsl(var(--foreground));
font-size: var(--text-ui);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
@@ -66,13 +216,13 @@ body {
}
a {
- color: var(--accent);
+ color: hsl(var(--primary));
text-decoration: none;
transition: color 0.15s;
}
a:hover {
- color: var(--accent-hover);
+ color: hsl(var(--primary) / 0.8);
text-decoration: underline;
}
@@ -111,15 +261,24 @@ h3 {
/* ---- Cards ---- */
.card {
- background: var(--surface-1);
- border: 1px solid var(--border);
- border-radius: 2px;
+ background: hsl(var(--card));
+ border: 1px solid hsl(var(--border));
+ border-radius: var(--radius);
padding: 20px;
transition: border-color 0.15s;
}
.card:hover {
- border-color: var(--border-hover);
+ border-color: hsl(var(--smui-border-hover));
+}
+
+/* Card glow -- SMUI standard hover effect */
+.card-glow {
+ transition: border-color 0.15s;
+}
+
+.card-glow:hover {
+ border-color: hsl(var(--smui-border-hover));
}
/* ---- Badges ---- */
@@ -128,7 +287,7 @@ h3 {
display: inline-block;
padding: 1px 6px;
border: 1px solid transparent;
- border-radius: 2px;
+ border-radius: var(--radius);
font-size: var(--text-label);
font-weight: 500;
font-family: var(--font-mono);
@@ -137,20 +296,32 @@ h3 {
}
.badge-pass {
- color: var(--green);
- border-color: var(--green);
+ color: hsl(var(--smui-green));
+ border-color: hsl(var(--smui-green) / 0.3);
background: transparent;
}
.badge-fail {
- color: var(--red);
- border-color: var(--red);
+ color: hsl(var(--smui-red));
+ border-color: hsl(var(--smui-red) / 0.3);
background: transparent;
}
.badge-neutral {
- color: var(--accent);
- border-color: var(--accent);
+ color: hsl(var(--primary));
+ border-color: hsl(var(--primary) / 0.3);
+ background: transparent;
+}
+
+.badge-warn {
+ color: hsl(var(--smui-yellow));
+ border-color: hsl(var(--smui-yellow) / 0.3);
+ background: transparent;
+}
+
+.badge-info {
+ color: hsl(var(--smui-purple));
+ border-color: hsl(var(--smui-purple) / 0.3);
background: transparent;
}
@@ -165,12 +336,12 @@ table {
th, td {
padding: 8px 12px;
text-align: left;
- border-bottom: 1px solid var(--border);
+ border-bottom: 1px solid hsl(var(--border));
}
th {
- background: var(--surface-2);
- color: var(--text-muted);
+ background: hsl(var(--smui-surface-2));
+ color: hsl(var(--muted-foreground));
font-weight: 500;
font-size: var(--text-label);
text-transform: uppercase;
@@ -182,7 +353,7 @@ tr {
}
tr:hover {
- background: var(--surface-2);
+ background: hsl(var(--smui-surface-2));
}
/* ---- Score cells ---- */
@@ -192,41 +363,41 @@ tr:hover {
font-weight: 600;
}
-.score-high { color: var(--green); }
-.score-mid { color: var(--yellow); }
-.score-low { color: var(--red); }
+.score-high { color: hsl(var(--smui-green)); }
+.score-mid { color: hsl(var(--smui-yellow)); }
+.score-low { color: hsl(var(--smui-red)); }
/* ---- Form controls ---- */
select, input {
font-family: var(--font-mono);
- background: var(--surface-1);
- border: 1px solid var(--border);
- border-radius: 2px;
- color: var(--text);
+ background: hsl(var(--card));
+ border: 1px solid hsl(var(--input));
+ border-radius: var(--radius);
+ color: hsl(var(--foreground));
padding: 6px 10px;
font-size: var(--text-ui);
transition: border-color 0.15s;
}
select:hover, input:hover {
- border-color: var(--border-hover);
+ border-color: hsl(var(--smui-border-hover));
}
select:focus, input:focus {
outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 2px hsla(193, 44%, 67%, 0.25);
+ border-color: hsl(var(--ring));
+ box-shadow: 0 0 0 2px hsl(var(--ring) / 0.25);
}
/* ---- Buttons ---- */
button {
font-family: var(--font-mono);
- background: var(--surface-2);
- border: 1px solid var(--border);
- border-radius: 2px;
- color: var(--text);
+ background: hsl(var(--smui-surface-2));
+ border: 1px solid hsl(var(--border));
+ border-radius: var(--radius);
+ color: hsl(var(--foreground));
padding: 6px 14px;
font-size: var(--text-ui);
font-weight: 500;
@@ -237,13 +408,13 @@ button {
}
button:hover {
- border-color: var(--border-hover);
- background: var(--surface-3);
+ border-color: hsl(var(--smui-border-hover));
+ background: hsl(var(--smui-surface-3));
}
button:focus {
outline: none;
- box-shadow: 0 0 0 2px hsla(193, 44%, 67%, 0.25);
+ box-shadow: 0 0 0 2px hsl(var(--ring) / 0.25);
}
/* ---- Filters ---- */
@@ -263,7 +434,7 @@ button:focus {
.filter-group label {
font-size: var(--text-label);
- color: var(--text-muted);
+ color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 500;
@@ -279,30 +450,141 @@ button:focus {
}
.stat-card {
- background: var(--surface-1);
- border: 1px solid var(--border);
- border-radius: 2px;
+ background: hsl(var(--card));
+ border: 1px solid hsl(var(--border));
+ border-radius: var(--radius);
padding: 16px;
text-align: center;
transition: border-color 0.15s;
}
.stat-card:hover {
- border-color: var(--border-hover);
+ border-color: hsl(var(--smui-border-hover));
}
.stat-value {
font-size: var(--text-stat);
font-weight: 700;
font-family: var(--font-mono);
- color: var(--text);
+ color: hsl(var(--foreground));
+ letter-spacing: -0.5px;
}
.stat-label {
font-size: var(--text-label);
- color: var(--text-muted);
+ color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: 500;
margin-top: 4px;
}
+
+/* ---- Status dots ---- */
+
+.status-dot {
+ display: inline-block;
+ width: 5px;
+ height: 5px;
+ border-radius: 9999px;
+}
+
+.status-dot-green { background: hsl(var(--smui-green)); }
+.status-dot-yellow { background: hsl(var(--smui-yellow)); }
+.status-dot-orange { background: hsl(var(--smui-orange)); }
+.status-dot-red { background: hsl(var(--smui-red)); }
+.status-dot-purple { background: hsl(var(--smui-purple)); }
+
+/* ---- Keyboard shortcuts ---- */
+
+kbd {
+ font-family: var(--font-mono);
+ font-size: var(--text-label);
+ color: hsl(var(--muted-foreground));
+ border: 1px solid hsl(var(--border));
+ padding: 1px 4px;
+ background: hsl(var(--background));
+}
+
+/* ---- Alerts ---- */
+
+.alert {
+ border: 1px solid hsl(var(--border));
+ border-radius: var(--radius);
+ padding: 12px 16px;
+ font-size: var(--text-ui);
+}
+
+.alert-info {
+ border-color: hsl(var(--smui-frost-2) / 0.25);
+ background: hsl(var(--smui-frost-2) / 0.04);
+ color: hsl(var(--smui-frost-2));
+}
+
+.alert-success {
+ border-color: hsl(var(--smui-green) / 0.25);
+ background: hsl(var(--smui-green) / 0.04);
+ color: hsl(var(--smui-green));
+}
+
+.alert-warning {
+ border-color: hsl(var(--smui-yellow) / 0.25);
+ background: hsl(var(--smui-yellow) / 0.04);
+ color: hsl(var(--smui-yellow));
+}
+
+.alert-error {
+ border-color: hsl(var(--smui-red) / 0.25);
+ background: hsl(var(--smui-red) / 0.04);
+ color: hsl(var(--smui-red));
+}
+
+/* ---- Skeleton shimmer ---- */
+
+@keyframes shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
+
+.skeleton {
+ background: linear-gradient(90deg,
+ hsl(var(--smui-surface-2)) 25%,
+ hsl(var(--smui-surface-3)) 50%,
+ hsl(var(--smui-surface-2)) 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+/* ---- Theme toggle button ---- */
+
+.theme-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ background: transparent;
+ border: 1px solid hsl(var(--border));
+ border-radius: var(--radius);
+ color: hsl(var(--muted-foreground));
+ cursor: pointer;
+ transition: border-color 0.15s, color 0.15s;
+}
+
+.theme-toggle:hover {
+ border-color: hsl(var(--smui-border-hover));
+ color: hsl(var(--foreground));
+ background: hsl(var(--smui-surface-2));
+}
+
+.theme-toggle svg {
+ width: 14px;
+ height: 14px;
+}
+
+/* Hide the inactive icon */
+.theme-toggle .icon-sun { display: none; }
+.theme-toggle .icon-moon { display: block; }
+
+[data-theme="light"] .theme-toggle .icon-sun { display: block; }
+[data-theme="light"] .theme-toggle .icon-moon { display: none; }