From 2e3f9420d71384479c588e08bd8cb02afff008a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:15:22 +0000 Subject: [PATCH 1/2] Initial plan From 4fcf4392267643c988a1130d3a80d2b6f074541f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:28:18 +0000 Subject: [PATCH 2/2] Implement fully functional dark/light theme toggle with persistence and accessibility Co-authored-by: rezwana-karim <126201034+rezwana-karim@users.noreply.github.com> --- src/app/globals.css | 61 ++++++++++++++------------- src/app/layout.tsx | 15 ++++--- src/hooks/use-theme.ts | 48 ++------------------- src/lib/theme-context.tsx | 88 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 79 deletions(-) create mode 100644 src/lib/theme-context.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 7db674e..152a468 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -81,6 +81,39 @@ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); } +/* Dark mode variables */ +.dark { + --background: #0a0a0a; + --foreground: #ededed; + --muted: #1a1a1a; + --muted-foreground: #a1a1aa; + --popover: #0a0a0a; + --popover-foreground: #ededed; + --card: #0a0a0a; + --card-foreground: #ededed; + --border: #27272a; + --input: #27272a; + --primary: #ededed; + --primary-foreground: #0a0a0a; + --secondary: #1a1a1a; + --secondary-foreground: #ededed; + --accent: #1a1a1a; + --accent-foreground: #ededed; + --destructive: #ef4444; + --destructive-foreground: #ffffff; + --success: #10b981; + --success-foreground: #ffffff; + --warning: #f59e0b; + --warning-foreground: #ffffff; + --ring: #ededed; + + /* Dark mode shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3); +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); @@ -106,34 +139,6 @@ --border-radius: var(--radius); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - --muted: #1a1a1a; - --muted-foreground: #a1a1aa; - --popover: #0a0a0a; - --popover-foreground: #ededed; - --card: #0a0a0a; - --card-foreground: #ededed; - --border: #27272a; - --input: #27272a; - --primary: #ededed; - --primary-foreground: #0a0a0a; - --secondary: #1a1a1a; - --secondary-foreground: #ededed; - --accent: #1a1a1a; - --accent-foreground: #ededed; - --destructive: #ef4444; - --destructive-foreground: #ffffff; - --success: #10b981; - --success-foreground: #ffffff; - --warning: #f59e0b; - --warning-foreground: #ffffff; - --ring: #ededed; - } -} - body { background: var(--background); color: var(--foreground); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fb7e5f0..186c4b6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { Header } from "@/components/layout/header"; import { Footer } from "@/components/layout/footer"; import { SkipLinks } from "@/components/ui/skip-links"; +import { ThemeProvider } from "@/lib/theme-context"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -55,12 +56,14 @@ export default function RootLayout({ - -
-
-
{children}
-
-
+ + +
+
+
{children}
+
+
+
); diff --git a/src/hooks/use-theme.ts b/src/hooks/use-theme.ts index c825a37..8f04f73 100644 --- a/src/hooks/use-theme.ts +++ b/src/hooks/use-theme.ts @@ -1,51 +1,9 @@ "use client" -import { useState, useEffect } from 'react' +import { useThemeContext } from '@/lib/theme-context' -export type Theme = 'light' | 'dark' +export type { Theme } from '@/lib/theme-context' export function useTheme() { - const [theme, setTheme] = useState('light') - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - - // Check for stored theme preference or default to system preference - const stored = localStorage.getItem('theme') as Theme | null - if (stored) { - setTheme(stored) - document.documentElement.classList.toggle('dark', stored === 'dark') - } else { - // Check system preference - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - const systemTheme: Theme = prefersDark ? 'dark' : 'light' - setTheme(systemTheme) - document.documentElement.classList.toggle('dark', systemTheme === 'dark') - } - }, []) - - const toggleTheme = () => { - if (!mounted) return - - const newTheme: Theme = theme === 'light' ? 'dark' : 'light' - setTheme(newTheme) - localStorage.setItem('theme', newTheme) - document.documentElement.classList.toggle('dark', newTheme === 'dark') - } - - const setThemeValue = (newTheme: Theme) => { - if (!mounted) return - - setTheme(newTheme) - localStorage.setItem('theme', newTheme) - document.documentElement.classList.toggle('dark', newTheme === 'dark') - } - - return { - theme, - toggleTheme, - setTheme: setThemeValue, - mounted, - } + return useThemeContext() } \ No newline at end of file diff --git a/src/lib/theme-context.tsx b/src/lib/theme-context.tsx new file mode 100644 index 0000000..ab1988b --- /dev/null +++ b/src/lib/theme-context.tsx @@ -0,0 +1,88 @@ +"use client" + +import React, { createContext, useContext, useState, useEffect } from 'react' + +export type Theme = 'light' | 'dark' + +interface ThemeContextType { + theme: Theme + toggleTheme: () => void + setTheme: (theme: Theme) => void + mounted: boolean +} + +const ThemeContext = createContext(undefined) + +interface ThemeProviderProps { + children: React.ReactNode +} + +export function ThemeProvider({ children }: ThemeProviderProps) { + const [theme, setThemeState] = useState('light') + const [mounted, setMounted] = useState(false) + + // Initialize theme on mount + useEffect(() => { + setMounted(true) + + // Check for stored theme preference or default to system preference + const stored = localStorage.getItem('theme') as Theme | null + if (stored && (stored === 'light' || stored === 'dark')) { + setThemeState(stored) + applyTheme(stored) + } else { + // Check system preference + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const systemTheme: Theme = prefersDark ? 'dark' : 'light' + setThemeState(systemTheme) + applyTheme(systemTheme) + } + }, []) + + const applyTheme = (newTheme: Theme) => { + const root = document.documentElement + if (newTheme === 'dark') { + root.classList.add('dark') + } else { + root.classList.remove('dark') + } + } + + const toggleTheme = () => { + if (!mounted) return + + const newTheme: Theme = theme === 'light' ? 'dark' : 'light' + setThemeState(newTheme) + localStorage.setItem('theme', newTheme) + applyTheme(newTheme) + } + + const setTheme = (newTheme: Theme) => { + if (!mounted) return + + setThemeState(newTheme) + localStorage.setItem('theme', newTheme) + applyTheme(newTheme) + } + + const value: ThemeContextType = { + theme, + toggleTheme, + setTheme, + mounted, + } + + return ( + + {children} + + ) +} + +export function useThemeContext(): ThemeContextType { + const context = useContext(ThemeContext) + if (context === undefined) { + throw new Error('useThemeContext must be used within a ThemeProvider') + } + return context +} \ No newline at end of file