From e34bcea135471871d396eb1ce19f8b4aae72318e Mon Sep 17 00:00:00 2001 From: diana Date: Wed, 8 Apr 2026 10:16:22 +0200 Subject: [PATCH 1/2] feat: Add dark/light theme toggle with localStorage persistence Implement theme switcher functionality: - Add useTheme hook with localStorage persistence - Add light theme CSS variables alongside dark theme - Add theme toggle button in Topbar with Sun/Moon icons - Respects system color scheme preference as fallback - Smooth transitions between themes Files modified: - frontend/src/app/globals.css: Added theme variables - frontend/src/components/Topbar.tsx: Added toggle button - frontend/src/lib/useTheme.ts: New theme hook (created) The theme preference is stored in localStorage and restored on page load. Falls back to system preference if no saved preference exists. Closes #6 --- frontend/src/app/globals.css | 41 ++++++++++++++++++++++++++++-- frontend/src/components/Topbar.tsx | 12 ++++++++- frontend/src/lib/useTheme.ts | 25 ++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/useTheme.ts diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e777f75..19a907e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -160,13 +160,50 @@ --yellow: #d29922; } +/* Light theme */ +[data-theme='light'] { + --background: #ffffff; + --surface-1: #f6f8fa; + --surface-2: #eaeef2; + --border-1: #d0d7de; + --border-2: #d8dee4; + --border-3: #b1bac4; + --text-1: #24292f; + --text-2: #424a53; + --text-3: #656d76; + --blue: #0969da; + --purple: #8250df; + --green: #1a7f37; + --red: #d1242f; + --yellow: #9e6a03; +} + +/* Dark theme (default) */ +[data-theme='dark'] { + --background: #0d1117; + --surface-1: #161b22; + --surface-2: #0d1117; + --border-1: #30363d; + --border-2: #21262d; + --border-3: #444c56; + --text-1: #e6edf3; + --text-2: #c9d1d9; + --text-3: #8b949e; + --blue: #4f8ef7; + --purple: #7c5cfc; + --green: #3fb950; + --red: #f85149; + --yellow: #d29922; +} + + * { box-sizing: border-box; margin: 0; padding: 0; } html { scroll-behavior: smooth; } body { - background: #0d1117; - color: #e6edf3; + background: var(--background); + color: var(--text-1); font-family: 'Silkscreen', system-ui, sans-serif; -webkit-font-smoothing: antialiased; } diff --git a/frontend/src/components/Topbar.tsx b/frontend/src/components/Topbar.tsx index a8b5ba6..c56862d 100644 --- a/frontend/src/components/Topbar.tsx +++ b/frontend/src/components/Topbar.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Loader2, CheckCircle, XCircle, Github, Terminal } from 'lucide-react'; +import { Loader2, CheckCircle, XCircle, Github, Terminal, Sun, Moon } from 'lucide-react'; +import { useTheme } from '@/lib/useTheme'; import { Logo } from './Logo'; import type { ScanStatus } from '@/lib/types'; @@ -28,6 +29,7 @@ function useDateTime() { export function Topbar({ status, onHome }: Props) { const { date, time } = useDateTime(); + const { theme, toggleTheme } = useTheme(); return (
@@ -77,6 +79,14 @@ export function Topbar({ status, onHome }: Props) { )}
+ ('dark'); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + // Get saved theme from localStorage or system preference + const saved = localStorage.getItem('theme') as 'light' | 'dark' | null; + const initial = saved || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); + + setTheme(initial); + document.documentElement.setAttribute('data-theme', initial); + setMounted(true); + }, []); + + const toggleTheme = () => { + const newTheme = theme === 'dark' ? 'light' : 'dark'; + setTheme(newTheme); + localStorage.setItem('theme', newTheme); + document.documentElement.setAttribute('data-theme', newTheme); + }; + + return { theme, toggleTheme, mounted }; +} From 0a6f88da3bd70fc1675b4acbe68052b0fa6a1f2a Mon Sep 17 00:00:00 2001 From: diana Date: Wed, 8 Apr 2026 14:44:04 +0200 Subject: [PATCH 2/2] fix: Address owner review feedback on theme toggle 1. FOUC: Add blocking inline script in layout.tsx that reads localStorage/matchMedia and sets data-theme before React hydrates 2. CSS transition: Add transition: background-color 0.2s ease, color 0.2s ease to body rule in globals.css 3. mounted: Use mounted flag in Topbar to guard icon render and prevent Sun/Moon mismatch before hydration (falls back to Sun) --- frontend/src/app/globals.css | 1 + frontend/src/app/layout.tsx | 5 +++++ frontend/src/components/Topbar.tsx | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 19a907e..65ad00e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -205,6 +205,7 @@ body { background: var(--background); color: var(--text-1); font-family: 'Silkscreen', system-ui, sans-serif; + transition: background-color 0.2s ease, color 0.2s ease; -webkit-font-smoothing: antialiased; } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 8eed44a..bf4d4fc 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -56,6 +56,11 @@ export default function RootLayout({ children }: { children: React.ReactNode }) }), }} /> +