feat: add dark/light theme switching with ThemeContext and localStorage persistence

This commit is contained in:
Sanaullah-Turab
2026-09-09 18:12:16 +05:00
parent 54e140e4ed
commit 1a45cf5075
5 changed files with 135 additions and 38 deletions
+46
View File
@@ -0,0 +1,46 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(() => {
const saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') return saved;
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
return 'dark';
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => (prev === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
+19 -7
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React from 'react';
import {
Activity,
Compass,
@@ -6,8 +6,11 @@ import {
Cpu,
BarChart3,
Sliders,
Radio
Radio,
Sun,
Moon
} from 'lucide-react';
import { useTheme } from '../ThemeContext';
interface NavbarProps {
activeTab: string;
@@ -16,6 +19,7 @@ interface NavbarProps {
}
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineStatus = true }) => {
const { theme, toggleTheme } = useTheme();
const navItems = [
{ id: 'dashboard', label: 'Control Room', icon: Activity },
{ id: 'playground', label: 'Playground', icon: Compass },
@@ -27,7 +31,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
];
return (
<header className="sticky top-0 z-50 w-full border-b border-[#1F2833] bg-[#07090C]/90 backdrop-blur-md">
<header className="sticky top-0 z-50 w-full border-b border-border-subtle bg-bg-primary/90 backdrop-blur-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
{/* Brand / Logo */}
@@ -35,7 +39,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
onClick={() => setActiveTab('dashboard')}
className="flex items-center gap-3 cursor-pointer select-none shrink-0 group"
>
<div className="w-10 h-10 rounded-xl bg-[#0D1117] border border-accent-primary/40 flex items-center justify-center overflow-hidden shadow-[0_0_18px_rgba(56,148,255,0.25)] group-hover:border-accent-primary transition-all p-1">
<div className="w-10 h-10 rounded-xl bg-bg-secondary border border-accent-primary/40 flex items-center justify-center overflow-hidden shadow-[0_0_18px_rgba(56,148,255,0.25)] group-hover:border-accent-primary transition-all p-1">
<img
src="/logo.png"
alt="Model Router Logo"
@@ -59,7 +63,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
</div>
{/* Navigation Tabs */}
<nav className="hidden lg:flex items-center gap-1.5 p-1 rounded-xl bg-[#0D1117]/80 border border-[#1F2833]/80">
<nav className="hidden lg:flex items-center gap-1.5 p-1 rounded-xl bg-bg-secondary/80 border border-border-subtle/80">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
@@ -70,7 +74,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-mono font-semibold transition-all duration-150 whitespace-nowrap ${
isActive
? 'bg-accent-primary text-bg-primary shadow-[0_0_12px_rgba(56,148,255,0.4)] font-bold'
: 'text-text-muted hover:text-text-primary hover:bg-[#141920]'
: 'text-text-muted hover:text-text-primary hover:bg-bg-card'
}`}
>
<Icon className={`w-3.5 h-3.5 ${isActive ? 'text-bg-primary stroke-[2.5]' : 'text-text-muted stroke-[2]'}`} />
@@ -104,7 +108,15 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
{/* Status Indicator */}
<div className="hidden sm:flex items-center gap-2 shrink-0">
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-[#0D1117] border border-[#1F2833] font-mono text-xs shadow-inner">
<button
onClick={toggleTheme}
className="p-1.5 rounded-xl bg-bg-secondary border border-border-subtle text-text-muted hover:text-text-primary hover:border-border-active transition-all"
title="Toggle theme"
>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</button>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-bg-secondary border border-border-subtle font-mono text-xs shadow-inner">
<span className={`w-2 h-2 rounded-full ${onlineStatus ? 'bg-accent-success shadow-[0_0_8px_#10B981]' : 'bg-accent-error'}`} />
<span className="text-[11px] font-bold tracking-wide text-text-primary whitespace-nowrap">
ROUTER READY
+43 -19
View File
@@ -1,17 +1,17 @@
@import "tailwindcss";
@import "tailwindcss";
@theme {
--color-bg-primary: #07090C;
--color-bg-secondary: #0D1117;
--color-bg-card: #121820;
--color-bg-card-hover: #17202B;
--color-bg-primary: var(--theme-bg-primary);
--color-bg-secondary: var(--theme-bg-secondary);
--color-bg-card: var(--theme-bg-card);
--color-bg-card-hover: var(--theme-bg-card-hover);
--color-border-subtle: #1F2833;
--color-border-active: #324050;
--color-border-subtle: var(--theme-border-subtle);
--color-border-active: var(--theme-border-active);
--color-text-primary: #F0F4F8;
--color-text-muted: #8B98A5;
--color-text-dim: #546270;
--color-text-primary: var(--theme-text-primary);
--color-text-muted: var(--theme-text-muted);
--color-text-dim: var(--theme-text-dim);
--color-accent-primary: #3894FF;
--color-accent-secondary: #F59E0B;
@@ -24,19 +24,43 @@
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
:root {
--theme-bg-primary: #F8FAFC;
--theme-bg-secondary: #F1F5F9;
--theme-bg-card: #FFFFFF;
--theme-bg-card-hover: #F8FAFC;
--theme-border-subtle: #E2E8F0;
--theme-border-active: #CBD5E1;
--theme-text-primary: #0F172A;
--theme-text-muted: #475569;
--theme-text-dim: #64748B;
}
.dark {
--theme-bg-primary: #07090C;
--theme-bg-secondary: #0D1117;
--theme-bg-card: #121820;
--theme-bg-card-hover: #17202B;
--theme-border-subtle: #1F2833;
--theme-border-active: #324050;
--theme-text-primary: #F0F4F8;
--theme-text-muted: #8B98A5;
--theme-text-dim: #546270;
}
body {
background-color: #07090C;
color: #F0F4F8;
background-color: var(--theme-bg-primary);
color: var(--theme-text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
overflow-x: hidden;
}
/* Custom glow borders and glass effects */
.glass-panel {
background: rgba(18, 24, 32, 0.75);
background: color-mix(in srgb, var(--theme-bg-card) 75%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(40, 52, 68, 0.6);
border: 1px solid color-mix(in srgb, var(--theme-border-active) 60%, transparent);
}
.glass-panel-hover:hover {
@@ -48,8 +72,8 @@ body {
.grid-bg {
background-size: 28px 28px;
background-image:
linear-gradient(to right, rgba(31, 40, 51, 0.4) 1px, transparent 1px),
linear-gradient(to bottom, rgba(31, 40, 51, 0.4) 1px, transparent 1px);
linear-gradient(to right, color-mix(in srgb, var(--theme-border-subtle) 40%, transparent) 1px, transparent 1px),
linear-gradient(to bottom, color-mix(in srgb, var(--theme-border-subtle) 40%, transparent) 1px, transparent 1px);
}
.radar-glow {
@@ -83,12 +107,12 @@ body {
height: 6px;
}
::-webkit-scrollbar-track {
background: #07090C;
background: var(--theme-bg-primary);
}
::-webkit-scrollbar-thumb {
background: #1F2833;
background: var(--theme-border-subtle);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #324050;
background: var(--theme-border-active);
}
+5 -2
View File
@@ -1,10 +1,13 @@
import React from 'react'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { ThemeProvider } from './ThemeContext'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
)
+22 -10
View File
@@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import { Sliders, Shield, DollarSign, Cpu, CheckCircle } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { Sliders, Shield, DollarSign, Cpu, CheckCircle, Sun, Moon } from 'lucide-react';
import { fetchApi } from '../lib/api';
import { useTheme } from '../ThemeContext';
export const SettingsPage: React.FC = () => {
const { theme, toggleTheme } = useTheme();
const [budget, setBudget] = useState<{
daily_limit: number;
monthly_limit: number;
@@ -17,14 +19,24 @@ export const SettingsPage: React.FC = () => {
return (
<div className="space-y-8 max-w-4xl mx-auto pb-16 font-mono">
<div className="border-b border-border-subtle pb-4">
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
<Sliders className="w-5 h-5 text-accent-primary" />
SYSTEM CONFIGURATION & BUDGET GUARDS
</h2>
<p className="text-xs text-text-muted mt-1">
Configure daily and monthly spend limits, routing threshold interventions, and environment parameters.
</p>
<div className="border-b border-border-subtle pb-4 flex justify-between items-start">
<div>
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
<Sliders className="w-5 h-5 text-accent-primary" />
SYSTEM CONFIGURATION & BUDGET GUARDS
</h2>
<p className="text-xs text-text-muted mt-1">
Configure daily and monthly spend limits, routing threshold interventions, and environment parameters.
</p>
</div>
<button
onClick={toggleTheme}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-subtle text-text-muted hover:text-text-primary hover:border-border-active transition-all text-xs font-bold"
>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
{theme === 'dark' ? 'LIGHT MODE' : 'DARK MODE'}
</button>
</div>
{/* Budget Management */}