128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
|
|
import { twMerge } from "tailwind-merge";
|
|
import { Moon, Sun } from "lucide-react";
|
|
import type { TWUIButtonSizeType } from "../layout/Button";
|
|
|
|
const SizeMap: Record<
|
|
TWUIButtonSizeType,
|
|
{ icon: number; className: string }
|
|
> = {
|
|
smaller: { icon: 16, className: "w-4 h-4" },
|
|
small: { icon: 20, className: "w-5 h-5" },
|
|
normal: { icon: 24, className: "w-6 h-6" },
|
|
large: { icon: 28, className: "w-7 h-7" },
|
|
larger: { icon: 32, className: "w-8 h-8" },
|
|
};
|
|
|
|
type Props = DetailedHTMLProps<
|
|
HTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
> & {
|
|
active?: boolean;
|
|
setActive?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
iconWrapperProps?: DetailedHTMLProps<
|
|
HTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
>;
|
|
defaultScheme?: "light" | "dark";
|
|
/**
|
|
* Icon size. Matches the twui `Button` size scale.
|
|
* @default "normal"
|
|
*/
|
|
size?: TWUIButtonSizeType;
|
|
};
|
|
|
|
/**
|
|
* # Color Scheme Loader
|
|
* @className_wrapper twui-color-scheme-selector
|
|
*/
|
|
export default function ColorSchemeSelector({
|
|
active: initialActive,
|
|
setActive: externalSetActive,
|
|
iconWrapperProps,
|
|
defaultScheme,
|
|
size = "normal",
|
|
...props
|
|
}: Props) {
|
|
const { icon: iconSize, className: sizeClassName } = SizeMap[size];
|
|
const [active, setActive] = React.useState(initialActive);
|
|
|
|
React.useEffect(() => {
|
|
const isDocumentDark =
|
|
document.documentElement.classList.contains("dark");
|
|
const isDocumentLight =
|
|
document.documentElement.classList.contains("light");
|
|
|
|
if (isDocumentDark) {
|
|
setActive(true);
|
|
return;
|
|
} else if (isDocumentLight) {
|
|
setActive(false);
|
|
return;
|
|
}
|
|
|
|
const existingTheme = localStorage.getItem("theme");
|
|
|
|
if (existingTheme === "dark") {
|
|
setActive(true);
|
|
} else if (existingTheme === "light") {
|
|
setActive(false);
|
|
} else if (defaultScheme) {
|
|
setActive(defaultScheme == "dark");
|
|
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
setActive(true);
|
|
} else if (typeof active == "undefined") {
|
|
setActive(false);
|
|
}
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
if (typeof active == "undefined") return;
|
|
|
|
document.documentElement.classList.remove("dark", "light");
|
|
document.documentElement.classList.add(active ? "dark" : "light");
|
|
}, [active]);
|
|
|
|
/**
|
|
* Only an explicit toggle is persisted, so visitors who never choose
|
|
* keep following their OS preference.
|
|
*/
|
|
function toggleScheme() {
|
|
const next = !active;
|
|
setActive(next);
|
|
externalSetActive?.(next);
|
|
localStorage.setItem("theme", next ? "dark" : "light");
|
|
}
|
|
|
|
return (
|
|
<div
|
|
{...props}
|
|
className={twMerge(
|
|
"flex flex-row items-center",
|
|
"twui-color-scheme-selector",
|
|
props.className,
|
|
)}
|
|
>
|
|
<button
|
|
title="Color Scheme Selector Button"
|
|
onClick={toggleScheme}
|
|
className={twMerge(
|
|
"cursor-pointer hover:opacity-70 flex items-center justify-center",
|
|
)}
|
|
>
|
|
<div
|
|
{...iconWrapperProps}
|
|
className={twMerge(
|
|
"flex items-center justify-center",
|
|
sizeClassName,
|
|
iconWrapperProps?.className,
|
|
)}
|
|
>
|
|
{active == false && <Sun size={iconSize} />}
|
|
{active == true && <Moon size={iconSize} />}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|