From a4aa968434be047bcf0bd193f9c8a4e59d17c603 Mon Sep 17 00:00:00 2001 From: agent-company Date: Thu, 26 Mar 2026 10:15:11 +0000 Subject: [PATCH] feat: add dark/light mode toggle with localStorage persistence - Enable Tailwind "class" dark mode strategy - Use CSS custom properties for theme colors (bg, text, border) - Add ThemeProvider context with toggle and localStorage persistence - Add Sun/Moon toggle button in the header navigation - Inline script in index.html prevents FOUC on page load - All pages (Layout, Login, Register, ProtectedRoute) support both modes - Default theme follows system preference (prefers-color-scheme) Closes leeworks-agents/SPARC#33 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/index.html | 9 ++++ frontend/src/App.tsx | 3 ++ frontend/src/components/Layout.tsx | 13 +++++- frontend/src/components/ProtectedRoute.tsx | 2 +- frontend/src/context/ThemeContext.tsx | 48 ++++++++++++++++++++++ frontend/src/index.css | 24 ++++++++++- frontend/src/pages/Login.tsx | 2 +- frontend/src/pages/Register.tsx | 2 +- frontend/tailwind.config.js | 13 +++--- 9 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 frontend/src/context/ThemeContext.tsx diff --git a/frontend/index.html b/frontend/index.html index 631e457..0ff0633 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,6 +7,15 @@ SPARC Dashboard +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c3426cd..c20ca32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider } from './context/AuthContext'; +import { ThemeProvider } from './context/ThemeContext'; import { Layout } from './components/Layout'; import { ProtectedRoute } from './components/ProtectedRoute'; import { Login } from './pages/Login'; @@ -22,6 +23,7 @@ const queryClient = new QueryClient({ function App() { return ( + @@ -61,6 +63,7 @@ function App() { + ); } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 501dc1f..bf18963 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,9 +1,11 @@ import { Outlet, NavLink, useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; -import { Search, Layers, BarChart3, Info, Users, LogOut } from 'lucide-react'; +import { useTheme } from '../context/ThemeContext'; +import { Search, Layers, BarChart3, Info, Users, LogOut, Sun, Moon } from 'lucide-react'; export function Layout() { const { user, isAdmin, logout } = useAuth(); + const { theme, toggleTheme } = useTheme(); const navigate = useNavigate(); const handleLogout = () => { @@ -23,7 +25,7 @@ export function Layout() { } return ( -
+
{/* Header */}
@@ -63,6 +65,13 @@ export function Layout() { {/* User menu */}
+
{user?.email}
{user?.role}
diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx index 667057d..7c4eac9 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/components/ProtectedRoute.tsx @@ -12,7 +12,7 @@ export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRout if (isLoading) { return ( -
+
); diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx new file mode 100644 index 0000000..ea7f091 --- /dev/null +++ b/frontend/src/context/ThemeContext.tsx @@ -0,0 +1,48 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +type Theme = 'light' | 'dark'; + +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +function getInitialTheme(): Theme { + const stored = localStorage.getItem('theme'); + if (stored === 'light' || stored === 'dark') return stored; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState(getInitialTheme); + + useEffect(() => { + const root = document.documentElement; + if (theme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + localStorage.setItem('theme', theme); + }, [theme]); + + const toggleTheme = () => { + setTheme((prev) => (prev === 'dark' ? 'light' : 'dark')); + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index b94918a..3ef8621 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,6 +2,26 @@ @tailwind components; @tailwind utilities; +/* Light mode (default) */ +:root { + --color-bg-dark: #f1f5f9; + --color-bg-card: #ffffff; + --color-bg-card-hover: #e2e8f0; + --color-text-primary: #0f172a; + --color-text-secondary: #475569; + --color-border: #cbd5e1; +} + +/* Dark mode */ +.dark { + --color-bg-dark: #0f172a; + --color-bg-card: #1e293b; + --color-bg-card-hover: #334155; + --color-text-primary: #f8fafc; + --color-text-secondary: #94a3b8; + --color-border: #334155; +} + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; @@ -15,7 +35,7 @@ body { } ::-webkit-scrollbar-track { - background: #1e293b; + background: var(--color-bg-card); } ::-webkit-scrollbar-thumb { @@ -30,5 +50,5 @@ body { /* Selection */ ::selection { background: rgba(99, 102, 241, 0.3); - color: #f8fafc; + color: var(--color-text-primary); } diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 7246839..da3f157 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -31,7 +31,7 @@ export function Login() { }; return ( -
+
{/* Brand */}
diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index b3d0a6a..dd08b8c 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -40,7 +40,7 @@ export function Register() { }; return ( -
+
{/* Brand */}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index c03684f..7587f56 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -4,6 +4,7 @@ export default { "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], + darkMode: 'class', theme: { extend: { colors: { @@ -16,15 +17,15 @@ export default { warning: '#f59e0b', error: '#ef4444', bg: { - dark: '#0f172a', - card: '#1e293b', - 'card-hover': '#334155', + dark: 'var(--color-bg-dark)', + card: 'var(--color-bg-card)', + 'card-hover': 'var(--color-bg-card-hover)', }, text: { - primary: '#f8fafc', - secondary: '#94a3b8', + primary: 'var(--color-text-primary)', + secondary: 'var(--color-text-secondary)', }, - border: '#334155', + border: 'var(--color-border)', }, }, },