feat(dashboard): add light/dark theme with sidebar toggle
CI / Check (pull_request) Successful in 10m12s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped

Introduces a light theme alongside the existing dark Obsidian Control
look, plus a sun/moon toggle in the sidebar footer.

The dashboard's CSS already drove every surface through custom
properties on :root, so the light theme is added as a second token set
under `:root[data-theme="light"]` and, in parallel, inside a
`@media (prefers-color-scheme: light)` block guarded by
`:not([data-theme="dark"])`. Net effect:
- A user with no stored preference gets their OS theme via the media
  query (no flash, no JS required).
- A user who clicked the toggle gets `data-theme="light|dark"` set on
  `<html>`, which wins over the media query.

The toggle component (`theme_toggle.rs`) reads `localStorage` first
then `prefers-color-scheme` on mount, and writes both the DOM
attribute and `localStorage` on click. All `web_sys` calls are gated
behind `#[cfg(feature = "web")]` so the server build stays clean.

Three CSS rules that hardcoded near-black hex values (the page dot
grid, `.code-block`, and the graph stabilization overlay) get explicit
light-mode overrides so they don't render as dark patches on white.

web-sys feature list extended with Storage, MediaQueryList, and
Element so the toggle can read the media query and set the attribute.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-05-13 13:26:37 +02:00
parent 927fbc8ecb
commit 4424db5acb
5 changed files with 247 additions and 1 deletions
+139
View File
@@ -61,6 +61,77 @@
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* ── Light theme tokens ──
Applied when the user has explicitly chosen light (`data-theme="light"`)
OR when their OS prefers light AND they have made no explicit choice. */
:root[data-theme="light"] {
--bg-primary: #f5f7fb;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.85);
--bg-card-solid: #ffffff;
--bg-card-hover: #f1f5fb;
--bg-elevated: #f8fafc;
--text-primary: #0c1426;
--text-secondary: #475569;
--text-tertiary: #8a9bb4;
--accent: #0070d4;
--accent-hover: #0080f0;
--accent-muted: rgba(0, 112, 212, 0.10);
--accent-glow: 0 0 20px rgba(0, 112, 212, 0.10);
--border: #e2e8f0;
--border-bright: #cbd5e1;
--border-accent: rgba(0, 112, 212, 0.30);
--danger: #dc2626;
--danger-bg: rgba(220, 38, 38, 0.08);
--warning: #d97706;
--warning-bg: rgba(217, 119, 6, 0.08);
--success: #16a34a;
--success-bg: rgba(22, 163, 74, 0.08);
--info: #2563eb;
--info-bg: rgba(37, 99, 235, 0.08);
--orange: #ea580c;
--orange-bg: rgba(234, 88, 12, 0.08);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg-primary: #f5f7fb;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.85);
--bg-card-solid: #ffffff;
--bg-card-hover: #f1f5fb;
--bg-elevated: #f8fafc;
--text-primary: #0c1426;
--text-secondary: #475569;
--text-tertiary: #8a9bb4;
--accent: #0070d4;
--accent-hover: #0080f0;
--accent-muted: rgba(0, 112, 212, 0.10);
--accent-glow: 0 0 20px rgba(0, 112, 212, 0.10);
--border: #e2e8f0;
--border-bright: #cbd5e1;
--border-accent: rgba(0, 112, 212, 0.30);
--danger: #dc2626;
--danger-bg: rgba(220, 38, 38, 0.08);
--warning: #d97706;
--warning-bg: rgba(217, 119, 6, 0.08);
--success: #16a34a;
--success-bg: rgba(22, 163, 74, 0.08);
--info: #2563eb;
--info-bg: rgba(37, 99, 235, 0.08);
--orange: #ea580c;
--orange-bg: rgba(234, 88, 12, 0.08);
}
}
/* ── Reset & Base ── */
@@ -396,6 +467,44 @@ code {
background: rgba(0, 200, 255, 0.06);
}
.theme-toggle {
background: none;
border: none;
border-top: 1px solid var(--border);
color: var(--text-secondary);
padding: 11px 18px;
cursor: pointer;
display: flex;
align-items: center;
gap: 11px;
font-family: var(--font-body);
font-size: 13.5px;
font-weight: 500;
transition: color 0.2s, background 0.2s;
width: 100%;
text-align: left;
}
.theme-toggle:hover {
color: var(--accent);
background: var(--accent-muted);
}
.theme-toggle svg {
flex-shrink: 0;
opacity: 0.75;
transition: opacity 0.2s;
}
.theme-toggle:hover svg {
opacity: 1;
}
.sidebar.collapsed .theme-toggle {
justify-content: center;
padding: 11px 0;
}
.sidebar.collapsed .sidebar-header {
padding: 22px 0;
justify-content: center;
@@ -3889,3 +3998,33 @@ tbody tr:last-child td {
.copyable code, .copyable .mono { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-snippet-wrapper { position: relative; }
.code-snippet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; gap: 8px; }
/* ═══════════════════════════════════════════════════════════════
LIGHT THEME — surface overrides for the few hardcoded dark
colors that don't go through CSS custom properties.
═══════════════════════════════════════════════════════════════ */
:root[data-theme="light"] .main-content {
background-image: radial-gradient(circle at 1px 1px, rgba(100, 116, 139, 0.18) 1px, transparent 0);
}
:root[data-theme="light"] .code-block {
background: #f8fafc;
color: #0c1426;
}
:root[data-theme="light"] .graph-stab-overlay {
background: radial-gradient(ellipse at center, rgba(245, 247, 251, 0.92) 0%, rgba(245, 247, 251, 0.98) 100%);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) .main-content {
background-image: radial-gradient(circle at 1px 1px, rgba(100, 116, 139, 0.18) 1px, transparent 0);
}
:root:not([data-theme="dark"]) .code-block {
background: #f8fafc;
color: #0c1426;
}
:root:not([data-theme="dark"]) .graph-stab-overlay {
background: radial-gradient(ellipse at center, rgba(245, 247, 251, 0.92) 0%, rgba(245, 247, 251, 0.98) 100%);
}
}