From e018e40db7c8feb7925044494163575fe3f65b44 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Wed, 9 Apr 2025 02:42:21 +0000 Subject: [PATCH] Implement automatic theme syncing based on system preferences Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/a42bd2e2-cfdc-48ae-9e9e-dea25be47f88.jpg --- client/index.html | 4 ++ client/public/favicon.svg | 10 ++++ client/src/components/theme-toggle.tsx | 83 +++++++++++++++++++++----- client/src/hooks/use-theme.tsx | 82 ++++++++++++++++++++----- 4 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 client/public/favicon.svg diff --git a/client/index.html b/client/index.html index a6d1d08..de11f05 100644 --- a/client/index.html +++ b/client/index.html @@ -3,6 +3,10 @@ + + + Active Directory Management +
diff --git a/client/public/favicon.svg b/client/public/favicon.svg new file mode 100644 index 0000000..95f71ab --- /dev/null +++ b/client/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/theme-toggle.tsx b/client/src/components/theme-toggle.tsx index 8bcce77..492bbf4 100644 --- a/client/src/components/theme-toggle.tsx +++ b/client/src/components/theme-toggle.tsx @@ -1,11 +1,12 @@ import * as React from "react"; -import { Moon, Sun, Laptop } from "lucide-react"; +import { Moon, Sun, Laptop, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { useTheme } from "@/hooks/use-theme"; @@ -28,29 +29,83 @@ export function ThemeToggle() { // Component that uses the theme context function ThemeToggleWithContext() { - const { theme, setTheme } = useTheme(); + const { theme, setTheme, resolvedTheme } = useTheme(); + + const iconClassName = "h-5 w-5 transition-all duration-200"; + + // Determine which icon to show in the button based on current theme + const getButtonIcon = () => { + if (theme === 'system') { + return resolvedTheme === 'dark' + ? + : ; + } else if (theme === 'dark') { + return ; + } else { + return ; + } + }; return ( - - setTheme("light")}> - - Light +
+ Theme +
+ + setTheme("light")} + className="flex justify-between" + > +
+ + Light +
+ {theme === "light" && }
- setTheme("dark")}> - - Dark + setTheme("dark")} + className="flex justify-between" + > +
+ + Dark +
+ {theme === "dark" && }
- setTheme("system")}> - - System + setTheme("system")} + className="flex justify-between" + > +
+ + System +
+ {theme === "system" && ( +
+ {resolvedTheme === "dark" ? ( + + ) : ( + + )} + +
+ )}
diff --git a/client/src/hooks/use-theme.tsx b/client/src/hooks/use-theme.tsx index 4c242d5..8c9ad4f 100644 --- a/client/src/hooks/use-theme.tsx +++ b/client/src/hooks/use-theme.tsx @@ -5,6 +5,7 @@ type Theme = 'dark' | 'light' | 'system'; interface ThemeContextType { theme: Theme; setTheme: (theme: Theme) => void; + resolvedTheme: 'dark' | 'light'; } const ThemeContext = createContext(undefined); @@ -24,25 +25,60 @@ export function ThemeProvider({ const [theme, setTheme] = useState( () => (localStorage.getItem(storageKey) as Theme) || defaultTheme ); + const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>( + window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + ); + // Apply the theme to the document useEffect(() => { const root = window.document.documentElement; root.classList.remove('light', 'dark'); + let effectiveTheme: 'light' | 'dark'; + if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'; - - root.classList.add(systemTheme); - return; + effectiveTheme = resolvedTheme; + } else { + effectiveTheme = theme as 'light' | 'dark'; } + + root.classList.add(effectiveTheme); - root.classList.add(theme); - }, [theme]); + // Add a transition for smoother theme changes + root.style.colorScheme = effectiveTheme; + + // Set meta theme-color for mobile browsers + document.querySelector('meta[name="theme-color"]')?.setAttribute( + 'content', + effectiveTheme === 'dark' ? '#020817' : '#ffffff' + ); + }, [theme, resolvedTheme]); + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + // Define handler for system theme changes + const handleSystemThemeChange = (event: MediaQueryListEvent) => { + const newSystemTheme = event.matches ? 'dark' : 'light'; + setResolvedTheme(newSystemTheme); + + // Log the change for debugging + console.log(`System theme changed to ${newSystemTheme}`); + }; + + // Add listener for system theme changes + mediaQuery.addEventListener('change', handleSystemThemeChange); + + // Clean up listener on component unmount + return () => { + mediaQuery.removeEventListener('change', handleSystemThemeChange); + }; + }, []); const value = { theme, + resolvedTheme, setTheme: (theme: Theme) => { localStorage.setItem(storageKey, theme); setTheme(theme); @@ -64,6 +100,9 @@ export const useTheme = (): ThemeContextType => { // This prevents crashes but won't sync across components const storageKey = 'ad-management-theme'; const defaultTheme = 'system'; + const resolvedSystemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; // Create a fallback state using localStorage const getThemeFromStorage = (): Theme => { @@ -77,19 +116,29 @@ export const useTheme = (): ThemeContextType => { const currentTheme = getThemeFromStorage(); + const getResolvedTheme = (theme: Theme): 'light' | 'dark' => { + if (theme === 'system') { + return resolvedSystemTheme; + } + return theme as 'light' | 'dark'; + }; + const applyThemeToDOM = (newTheme: Theme) => { try { const root = window.document.documentElement; root.classList.remove('light', 'dark'); - if (newTheme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'; - root.classList.add(systemTheme); - } else { - root.classList.add(newTheme); - } + const effectiveTheme = getResolvedTheme(newTheme); + root.classList.add(effectiveTheme); + + // Add a transition for smoother theme changes + root.style.colorScheme = effectiveTheme; + + // Set meta theme-color for mobile browsers + document.querySelector('meta[name="theme-color"]')?.setAttribute( + 'content', + effectiveTheme === 'dark' ? '#020817' : '#ffffff' + ); } catch (e) { console.error('Error applying theme to DOM:', e); } @@ -101,6 +150,7 @@ export const useTheme = (): ThemeContextType => { // Return a fallback implementation return { theme: currentTheme, + resolvedTheme: getResolvedTheme(currentTheme), setTheme: (newTheme: Theme) => { try { localStorage.setItem(storageKey, newTheme);