1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-30 07:16:37 -04:00

rename resolvedTheme -> activeTheme

This commit is contained in:
2022-05-04 10:48:41 -04:00
parent ef3071dd99
commit d67428b043
8 changed files with 38 additions and 43 deletions

View File

@ -22,7 +22,7 @@ export type CaptchaProps = {
const Captcha = ({ size = "normal", theme, className, ...rest }: CaptchaProps) => { const Captcha = ({ size = "normal", theme, className, ...rest }: CaptchaProps) => {
const hasMounted = useHasMounted(); const hasMounted = useHasMounted();
const { resolvedTheme } = useTheme(); const { activeTheme } = useTheme();
return ( return (
<div className={className}> <div className={className}>
@ -32,7 +32,7 @@ const Captcha = ({ size = "normal", theme, className, ...rest }: CaptchaProps) =
reCaptchaCompat={false} reCaptchaCompat={false}
tabIndex={0} tabIndex={0}
size={size} size={size}
theme={theme || (resolvedTheme === "dark" ? "dark" : "light")} theme={theme || (activeTheme === "dark" ? "dark" : "light")}
{...rest} {...rest}
/> />
)} )}

View File

@ -18,7 +18,7 @@ export type CommentsProps = ComponentProps<typeof Wrapper> & {
}; };
const Comments = ({ title, ...rest }: CommentsProps) => { const Comments = ({ title, ...rest }: CommentsProps) => {
const { resolvedTheme } = useTheme(); const { activeTheme } = useTheme();
// TODO: use custom `<Loading />` spinner component during suspense // TODO: use custom `<Loading />` spinner component during suspense
return ( return (
@ -29,7 +29,7 @@ const Comments = ({ title, ...rest }: CommentsProps) => {
mapping="specific" mapping="specific"
reactionsEnabled="1" reactionsEnabled="1"
emitMetadata="0" emitMetadata="0"
theme={resolvedTheme === "dark" ? "dark" : "light"} theme={activeTheme === "dark" ? "dark" : "light"}
/> />
</Wrapper> </Wrapper>
); );

View File

@ -33,13 +33,13 @@ export type LayoutProps = ComponentProps<typeof Flex> & {
}; };
const Layout = ({ container = true, children, ...rest }: LayoutProps) => { const Layout = ({ container = true, children, ...rest }: LayoutProps) => {
const { resolvedTheme } = useTheme(); const { activeTheme } = useTheme();
return ( return (
<> <>
<Head> <Head>
{/* dynamically set browser theme color to match the background color; default to light for SSR */} {/* dynamically set browser theme color to match the background color; default to light for SSR */}
<meta name="theme-color" content={themeColors[resolvedTheme === "dark" ? "dark" : "light"]} /> <meta name="theme-color" content={themeColors[activeTheme === "dark" ? "dark" : "light"]} />
</Head> </Head>
<Flex {...rest}> <Flex {...rest}>

View File

@ -1,13 +1,13 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { minify } from "uglify-js"; import { minify } from "uglify-js";
import { clientScript } from "./client"; import { clientScript } from "./client";
import { darkModeQuery, themeStorageKey, themeClassNames } from "../../lib/config/themes"; import { themeClassNames, themeStorageKey } from "../../lib/config/themes";
const ThemeScript = () => { const ThemeScript = () => {
const minified = useMemo(() => { const minified = useMemo(() => {
// since the client function will end up being injected as a plain dumb string, we need to set dynamic values here: // since the client function will end up being injected as a plain dumb string, we need to set dynamic values here:
const functionString = String(clientScript) const functionString = String(clientScript)
.replace('"__MEDIA_QUERY__"', `"${darkModeQuery}"`) .replace('"__MEDIA_QUERY__"', `"(prefers-color-scheme: dark)"`)
.replace('"__STORAGE_KEY__"', `"${themeStorageKey}"`) .replace('"__STORAGE_KEY__"', `"${themeStorageKey}"`)
.replace('"__CLASS_NAMES__"', JSON.stringify(themeClassNames)); .replace('"__CLASS_NAMES__"', JSON.stringify(themeClassNames));

View File

@ -1,4 +1,4 @@
import { useEffect, memo } from "react"; import { useEffect, useId, memo } from "react";
import { useMedia } from "react-use"; import { useMedia } from "react-use";
import { useSpring, animated, Globals } from "@react-spring/web"; import { useSpring, animated, Globals } from "@react-spring/web";
import { useTheme } from "../../hooks/use-theme"; import { useTheme } from "../../hooks/use-theme";
@ -19,17 +19,17 @@ const Button = styled("button", {
}); });
export type ThemeToggleProps = { export type ThemeToggleProps = {
id?: string;
className?: string; className?: string;
}; };
const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => { const ThemeToggle = ({ className }: ThemeToggleProps) => {
const hasMounted = useHasMounted(); const hasMounted = useHasMounted();
const { activeTheme, setTheme } = useTheme();
const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false); const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false);
const { resolvedTheme, setTheme } = useTheme(); const maskId = useId(); // SSR-safe ID to cross-reference areas of the SVG
// default to light since `resolvedTheme` might be undefined // default to light since `activeTheme` might be undefined
const safeTheme = resolvedTheme === "dark" ? "dark" : "light"; const safeTheme = activeTheme === "dark" ? "dark" : "light";
// accessibility: skip animation if user prefers reduced motion // accessibility: skip animation if user prefers reduced motion
useEffect(() => { useEffect(() => {
@ -120,7 +120,7 @@ const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => {
}} }}
className={className} className={className}
> >
<mask id={`moon-mask-${id}`}> <mask id={`mask-${maskId}`}>
<rect x="0" y="0" width="100%" height="100%" fill="white" /> <rect x="0" y="0" width="100%" height="100%" fill="white" />
<animated.circle <animated.circle
r="9" r="9"
@ -135,7 +135,7 @@ const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => {
cx="12" cx="12"
cy="12" cy="12"
fill="currentColor" fill="currentColor"
mask={`url(#moon-mask-${id})`} mask={`url(#mask-${maskId})`}
// @ts-ignore // @ts-ignore
style={centerCircleProps} style={centerCircleProps}
/> />

View File

@ -8,7 +8,7 @@ export type TweetEmbedProps = {
}; };
const TweetEmbed = ({ id, options }: TweetEmbedProps) => { const TweetEmbed = ({ id, options }: TweetEmbedProps) => {
const { resolvedTheme } = useTheme(); const { activeTheme } = useTheme();
return ( return (
<TwitterTweetEmbed <TwitterTweetEmbed
@ -16,7 +16,7 @@ const TweetEmbed = ({ id, options }: TweetEmbedProps) => {
options={{ options={{
dnt: true, dnt: true,
align: "center", align: "center",
theme: resolvedTheme === "dark" ? "dark" : "light", theme: activeTheme === "dark" ? "dark" : "light",
...options, ...options,
}} }}
/> />

View File

@ -1,20 +1,16 @@
import { createContext, useCallback, useEffect, useState } from "react"; import { createContext, useCallback, useEffect, useState } from "react";
import { useLocalStorage, useMedia } from "react-use"; import { useLocalStorage, useMedia } from "react-use";
import { darkModeQuery, themeStorageKey } from "../lib/config/themes"; import { themeStorageKey } from "../lib/config/themes";
import type { Context, PropsWithChildren } from "react"; import type { Context, PropsWithChildren } from "react";
export const ThemeContext: Context<{ export const ThemeContext: Context<{
/** Update the theme manually. */
setTheme?: (theme: string) => void;
/** The user's website theme setting ("light" or "dark", or undefined if unset). */
preferredTheme?: string;
/** /**
* If the theme setting is undefined, this returns whether the system preference resolved to "light" or "dark". If the * If the user's theme preference is unset, this returns whether the system preference resolved to "light" or "dark".
* preference is set, the value is identical to `preferredTheme`. * If the user's theme preference is set, the preference is returned instead, regardless of their system's theme.
*
* Note to self: you probably want this.
*/ */
resolvedTheme?: string; activeTheme?: "light" | "dark";
/** Update the theme manually and save to local storage. */
setTheme?: (theme: string) => void;
}> = createContext({}); }> = createContext({});
// provider used once in _app.tsx to wrap entire app // provider used once in _app.tsx to wrap entire app
@ -27,12 +23,13 @@ export const ThemeProvider = ({
[themeName: string]: string; [themeName: string]: string;
}; };
}>) => { }>) => {
// keep track of if/when the user has set their theme *here*: // keep track of if/when the user has set their theme *on this site*
const [preferredTheme, setPreferredTheme] = useLocalStorage(themeStorageKey, null, { raw: true }); const [preferredTheme, setPreferredTheme] = useLocalStorage(themeStorageKey, null, { raw: true });
// save the end result no matter how we got there (by preference or by system): // keep track of changes to the user's OS/browser dark mode setting
const [resolvedTheme, setResolvedTheme] = useState(""); const [systemTheme, setSystemTheme] = useState("");
// hook into system `prefers-dark-mode` setting // hook into system `prefers-dark-mode` setting
const isSystemDark = useMedia(darkModeQuery, false); // https://web.dev/prefers-color-scheme/#the-prefers-color-scheme-media-query
const isSystemDark = useMedia("(prefers-color-scheme: dark)", false);
// get the theme names (light, dark) via passed-in classnames' keys // get the theme names (light, dark) via passed-in classnames' keys
const themeNames = Object.keys(classNames); const themeNames = Object.keys(classNames);
@ -54,28 +51,31 @@ export const ThemeProvider = ({
// listen for changes in OS preference // listen for changes in OS preference
useEffect(() => { useEffect(() => {
const systemTheme = isSystemDark ? "dark" : "light"; // translate boolean to theme string
const systemResolved = isSystemDark ? "dark" : "light";
// keep track of the resolved theme whether or not we change it below // keep track of the system theme whether or not we override it manually
setResolvedTheme(systemTheme); setSystemTheme(systemResolved);
// only actually change the theme if preference is unset (and *don't* save new theme to storage) // only actually change the theme if preference is unset (and *don't* save new theme to storage)
if (!preferredTheme || !themeNames.includes(preferredTheme)) { if (!preferredTheme || !themeNames.includes(preferredTheme)) {
changeTheme(systemTheme, false); changeTheme(systemResolved, false);
} }
}, [changeTheme, themeNames, preferredTheme, isSystemDark]); }, [changeTheme, themeNames, preferredTheme, isSystemDark]);
// color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.) // color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.)
useEffect(() => { useEffect(() => {
// only "light" and "dark" are valid here // only "light" and "dark" are valid here
const colorScheme = ["light", "dark"].includes(preferredTheme) ? preferredTheme : resolvedTheme; // https://web.dev/color-scheme/#the-color-scheme-css-property
const colorScheme = ["light", "dark"].includes(preferredTheme) ? preferredTheme : systemTheme;
document.documentElement.style?.setProperty("color-scheme", colorScheme); document.documentElement.style?.setProperty("color-scheme", colorScheme);
}, [preferredTheme, resolvedTheme]); }, [preferredTheme, systemTheme]);
return ( return (
<ThemeContext.Provider <ThemeContext.Provider
value={{ value={{
activeTheme: themeNames.includes(preferredTheme) ? preferredTheme : systemTheme,
setTheme: useCallback( setTheme: useCallback(
(theme: string) => { (theme: string) => {
// force save to local storage // force save to local storage
@ -83,8 +83,6 @@ export const ThemeProvider = ({
}, },
[changeTheme] [changeTheme]
), ),
preferredTheme: themeNames.includes(preferredTheme) ? preferredTheme : undefined,
resolvedTheme: themeNames.includes(preferredTheme) ? preferredTheme : resolvedTheme,
}} }}
> >
{children} {children}

View File

@ -12,8 +12,5 @@ export const themeColors = {
dark: darkTheme.colors.backgroundOuter?.value, dark: darkTheme.colors.backgroundOuter?.value,
}; };
// https://web.dev/prefers-color-scheme/#the-prefers-color-scheme-media-query
export const darkModeQuery = "(prefers-color-scheme: dark)";
// local storage key // local storage key
export const themeStorageKey = "preferred-theme"; export const themeStorageKey = "preferred-theme";