this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 89 lines 3.0 kB view raw
1"use client"; 2 3import * as React from "react"; 4 5type Theme = "system" | "light" | "dark"; 6 7function getSystemTheme(): "light" | "dark" { 8 if (typeof window === "undefined") return "light"; 9 return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; 10} 11 12function getResolvedTheme(theme: Theme): "light" | "dark" { 13 if (theme === "system") return getSystemTheme(); 14 return theme; 15} 16 17export function ThemeToggle() { 18 const [theme, setTheme] = React.useState<Theme>("system"); 19 const [isClicked, setIsClicked] = React.useState(false); 20 const [mounted, setMounted] = React.useState(false); 21 22 React.useEffect(() => { 23 setMounted(true); 24 const saved = localStorage.getItem("theme") as Theme | null; 25 if (saved) setTheme(saved); 26 }, []); 27 28 React.useEffect(() => { 29 const root = document.documentElement; 30 if (theme === "system") { 31 root.removeAttribute("data-theme"); 32 localStorage.removeItem("theme"); 33 } else { 34 root.setAttribute("data-theme", theme); 35 localStorage.setItem("theme", theme); 36 } 37 }, [theme]); 38 39 React.useEffect(() => { 40 const media = window.matchMedia("(prefers-color-scheme: dark)"); 41 const handler = () => { 42 if (theme === "system") { 43 // Trigger a re-render so any derived state updates 44 setTheme("system"); 45 } 46 }; 47 media.addEventListener("change", handler); 48 return () => media.removeEventListener("change", handler); 49 }, [theme]); 50 51 const cycleTheme = React.useCallback(() => { 52 setIsClicked(true); 53 setTimeout(() => setIsClicked(false), 150); 54 setTheme((prev) => (prev === "system" ? "light" : prev === "light" ? "dark" : "system")); 55 }, []); 56 57 const icon = theme === "system" ? "◐" : theme === "light" ? "○" : "●"; 58 const resolved = getResolvedTheme(theme); 59 60 // Prevent hydration mismatch by rendering a placeholder until mounted 61 if (!mounted) { 62 return ( 63 <button 64 className="flex items-center gap-2 px-3 py-2 text-sm font-mono text-(--text-muted) shrink-0" 65 disabled 66 aria-hidden="true" 67 > 68 <span></span> 69 <span className="hidden sm:inline">system</span> 70 </button> 71 ); 72 } 73 74 return ( 75 <button 76 onClick={cycleTheme} 77 className="flex items-center gap-2 px-3 py-2 text-sm font-mono text-(--text-secondary) transition-colors duration-fast ease-default hover:text-(--accent-default) hover:bg-(--accent-subtle) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-secondary) shrink-0" 78 title={`Theme: ${resolved} (click to cycle)`} 79 aria-label={`Current theme: ${resolved}. Click to cycle.`} 80 > 81 <span 82 className={`text-(--accent-default) inline-block ${isClicked ? "motion-safe:scale-[1.3]" : ""} transition-transform duration-fast`} 83 > 84 {icon} 85 </span> 86 <span className="hidden sm:inline">{theme}</span> 87 </button> 88 ); 89}