UNPKG

ginb

Version:

Write & Share all in GitHub

293 lines (276 loc) 9.78 kB
"use client"; import { useState, useEffect, useContext, createContext, useCallback, memo } from "react"; const ThemeContext = createContext(); export const useTheme = () => useContext(ThemeContext); export const ThemeProviders = memo(({ children }) => { const [theme, setTheme] = useState(() => { if (typeof window === "undefined") { return undefined; } const localTheme = localStorage.getItem("user-color-scheme"); return localTheme || "system"; }); const [colorScheme, setColorScheme] = useState(() => { if (typeof window === "undefined") { return undefined; } if (window.matchMedia("(prefers-color-scheme: dark)").matches) { return "dark"; } else { return "light"; } }); const handleColorSchemeChange = useCallback( (event) => { if (!event) { event = window.matchMedia("(prefers-color-scheme: dark)"); } if (event.matches) { setColorScheme("dark"); } else { setColorScheme("light"); } }, [setColorScheme], ); useEffect(() => { window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", handleColorSchemeChange); return () => { window .matchMedia("(prefers-color-scheme: dark)") .removeEventListener("change", handleColorSchemeChange); }; }, [handleColorSchemeChange]); useEffect(() => { if (theme == "system") { if (document.documentElement.className != colorScheme) { document.documentElement.className = colorScheme; } } }, [theme, colorScheme]); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <script dangerouslySetInnerHTML={{ __html: "!function(){const e=localStorage.getItem('user-color-scheme');'light'==e||'dark'==e?document.documentElement.classList.contains(e)||(document.documentElement.classList=[e]):document.documentElement.classList=[window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light']}();", }} /> {children} </ThemeContext.Provider> ); }); export const ToggleTheme = memo(({ responsive }) => { const [mounted, setMounted] = useState(false); const [isToggleOpen, setIsToggleOpen] = useState(false); const { theme, setTheme } = useTheme(); const toggleTheme = useCallback( (color) => { if (color != "system" && color != "light" && color != "dark") { console.error("color must be system, light or dark"); } else { setTheme(color); typeof window !== "undefined" && localStorage.setItem("user-color-scheme", color); if (color == "light" || color == "dark") { if (document.documentElement.className != color) { document.documentElement.className = color; } } } }, [setTheme], ); const handleClickOutside = useCallback( (e) => { if (e.target.closest("#toggle-theme-button") || e.target.closest("#toggle-theme-menu")) { return; } else { setIsToggleOpen(false); } }, [setIsToggleOpen], ); useEffect(() => { setMounted(true); window.addEventListener("click", handleClickOutside); return () => { window.removeEventListener("click", handleClickOutside); }; }, []); if (!mounted) { return null; } return ( <> <button id="toggle-theme-button" aria-label="Toggle Theme" className={ "text-[#666] hover:text-black focus:outline-none dark:text-[#888] dark:hover:text-white" + (responsive ? " " + responsive : "") } type="button" onClick={() => setIsToggleOpen(!isToggleOpen)} > <svg className="m-auto h-6 w-6" stroke="currentColor" fill="none" strokeWidth={2} viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" > <path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /> <path d="M12 3v18" /> <path d="M12 14l7 -7" /> <path d="M12 19l8.5 -8.5" /> <path d="M12 9l4.5 -4.5" /> </svg> </button> <div className={(isToggleOpen ? "absolute" : "hidden") + " right-0 top-12"}> <div id="toggle-theme-menu" className="mt-1 box-content block min-w-min rounded-lg border bg-white p-2 dark:bg-black" > <button id="toggle-theme-light-button" aria-label="Always Light" className="mx-auto flex w-full flex-row rounded-lg p-2 text-black/60 hover:bg-black/10 dark:text-white/60 dark:hover:bg-white/20" onClick={() => toggleTheme("light")} > {theme == "light" ? ( <svg className="my-auto h-4 w-6 px-1" fill="none" height="24" shapeRendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" > <path d="M 19.28125 5.28125 L 9 15.5625 L 4.71875 11.28125 L 3.28125 12.71875 L 8.28125 17.71875 L 9 18.40625 L 9.71875 17.71875 L 20.71875 6.71875 Z" /> </svg> ) : ( <span className="w-6"></span> )} <svg className="my-auto h-4 w-6 px-1" data-testid="geist-icon" fill="none" height="24" shapeRendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" > <circle cx="12" cy="12" r="5"></circle> <path d="M12 1v2"></path> <path d="M12 21v2"></path> <path d="M4.22 4.22l1.42 1.42"></path> <path d="M18.36 18.36l1.42 1.42"></path> <path d="M1 12h2"></path> <path d="M21 12h2"></path> <path d="M4.22 19.78l1.42-1.42"></path> <path d="M18.36 5.64l1.42-1.42"></path> </svg> <span className="break-keep">Light</span> </button> <button id="toggle-theme-dark-button" aria-label="Always Dark" className="mx-auto flex w-full flex-row rounded-lg p-2 text-black/60 hover:bg-black/10 dark:text-white/60 dark:hover:bg-white/20" onClick={() => toggleTheme("dark")} > {theme == "dark" ? ( <svg className="my-auto h-4 w-6 px-1" fill="none" height="24" shapeRendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" > <path d="M 19.28125 5.28125 L 9 15.5625 L 4.71875 11.28125 L 3.28125 12.71875 L 8.28125 17.71875 L 9 18.40625 L 9.71875 17.71875 L 20.71875 6.71875 Z" /> </svg> ) : ( <span className="w-6"></span> )} <svg className="my-auto h-4 w-6 px-1" data-testid="geist-icon" fill="none" height="24" shapeRendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" > <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path> </svg> <span className="break-keep">Dark</span> </button> <button id="toggle-theme-system-button" aria-label="Follow System" className="mx-auto flex w-full flex-row rounded-lg p-2 text-black/60 hover:bg-black/10 dark:text-white/60 dark:hover:bg-white/20" onClick={() => toggleTheme("system")} > {theme != "light" && theme != "dark" ? ( <svg className="my-auto h-4 w-6 px-1" fill="none" height="24" shapeRendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" > <path d="M 19.28125 5.28125 L 9 15.5625 L 4.71875 11.28125 L 3.28125 12.71875 L 8.28125 17.71875 L 9 18.40625 L 9.71875 17.71875 L 20.71875 6.71875 Z" /> </svg> ) : ( <span className="w-6"></span> )} <svg className="my-auto h-4 w-6 px-1" data-testid="geist-icon" fill="none" height="24" shapeRendering="geometricPrecision" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" > <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect> <path d="M8 21h8"></path> <path d="M12 17v4"></path> </svg> <span className="break-keep">System</span> </button> </div> </div> </> ); });