mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 11:25:22 -04:00
160 lines
4.3 KiB
TypeScript
160 lines
4.3 KiB
TypeScript
import { useEffect, memo } from "react";
|
|
import { useMedia } from "react-use";
|
|
import { useSpring, animated, Globals } from "@react-spring/web";
|
|
import { useTheme } from "../../hooks/use-theme";
|
|
import { useHasMounted } from "../../hooks/use-has-mounted";
|
|
import { styled } from "../../lib/styles/stitches.config";
|
|
|
|
const Button = styled("button", {
|
|
border: 0,
|
|
padding: "0.6em",
|
|
marginRight: "-0.6em",
|
|
background: "none",
|
|
cursor: "pointer",
|
|
color: "$mediumDark",
|
|
|
|
"&:hover": {
|
|
color: "$warning",
|
|
},
|
|
});
|
|
|
|
export type ThemeToggleProps = {
|
|
id?: string;
|
|
className?: string;
|
|
};
|
|
|
|
const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => {
|
|
const hasMounted = useHasMounted();
|
|
const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false);
|
|
const { resolvedTheme, setTheme } = useTheme();
|
|
|
|
// default to light since `resolvedTheme` might be undefined
|
|
const safeTheme = resolvedTheme === "dark" ? "dark" : "light";
|
|
|
|
// accessibility: skip animation if user prefers reduced motion
|
|
useEffect(() => {
|
|
Globals.assign({
|
|
skipAnimation: !!prefersReducedMotion,
|
|
});
|
|
}, [prefersReducedMotion]);
|
|
|
|
// modified from https://jfelix.info/blog/using-react-spring-to-animate-svg-icons-dark-mode-toggle
|
|
const springProperties = {
|
|
light: {
|
|
svg: {
|
|
transform: "rotate(90deg)",
|
|
},
|
|
circle: {
|
|
r: 5,
|
|
},
|
|
mask: {
|
|
cx: "100%",
|
|
cy: "0%",
|
|
},
|
|
lines: {
|
|
opacity: 1,
|
|
},
|
|
},
|
|
dark: {
|
|
svg: {
|
|
transform: "rotate(40deg)",
|
|
},
|
|
circle: {
|
|
r: 9,
|
|
},
|
|
mask: {
|
|
cx: "50%",
|
|
cy: "23%",
|
|
},
|
|
lines: {
|
|
opacity: 0,
|
|
},
|
|
},
|
|
springConfig: { mass: 4, tension: 250, friction: 35 },
|
|
};
|
|
|
|
const { svg, circle, mask, lines } = springProperties[safeTheme];
|
|
|
|
const svgContainerProps = useSpring({
|
|
...svg,
|
|
config: springProperties.springConfig,
|
|
});
|
|
const centerCircleProps = useSpring({
|
|
...circle,
|
|
config: springProperties.springConfig,
|
|
});
|
|
const maskedCircleProps = useSpring({
|
|
...mask,
|
|
config: springProperties.springConfig,
|
|
});
|
|
const linesProps = useSpring({
|
|
...lines,
|
|
config: springProperties.springConfig,
|
|
});
|
|
|
|
// render a blank div of the same size to avoid layout shifting until we're fully mounted and self-aware
|
|
if (!hasMounted) {
|
|
return (
|
|
<Button as="div" aria-hidden={true}>
|
|
<div className={className} />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
onClick={() => setTheme(safeTheme === "light" ? "dark" : "light")}
|
|
title={safeTheme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
|
|
aria-label={safeTheme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
|
|
>
|
|
<animated.svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
style={{
|
|
...svgContainerProps,
|
|
}}
|
|
className={className}
|
|
>
|
|
<mask id={`moon-mask-${id}`}>
|
|
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
|
<animated.circle
|
|
r="9"
|
|
fill="black"
|
|
// @ts-ignore
|
|
style={maskedCircleProps}
|
|
/>
|
|
</mask>
|
|
|
|
{/* circle shared by both the sun and crescent moon */}
|
|
<animated.circle
|
|
cx="12"
|
|
cy="12"
|
|
fill="currentColor"
|
|
mask={`url(#moon-mask-${id})`}
|
|
// @ts-ignore
|
|
style={centerCircleProps}
|
|
/>
|
|
|
|
{/* sunrays pulled from https://github.com/feathericons/feather/blob/734f3f51144e383cfdc6d0916831be8d1ad2a749/icons/sun.svg?short_path=fea872c#L13 */}
|
|
<animated.g stroke="currentColor" style={linesProps}>
|
|
<line x1="12" y1="1" x2="12" y2="3" />
|
|
<line x1="12" y1="21" x2="12" y2="23" />
|
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
<line x1="1" y1="12" x2="3" y2="12" />
|
|
<line x1="21" y1="12" x2="23" y2="12" />
|
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
</animated.g>
|
|
</animated.svg>
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
export default memo(ThemeToggle);
|