Vectrum
Sign In
58 lines · 1.9 KB · TypeScript
"use client";

import { useState, useSyncExternalStore } from "react";
import { Switcher } from "@unlocalhosted/metalui";
import { THEME_COOKIE, colorwayOf, type Theme } from "@/lib/theme";

const options = [
  { value: "light", label: "Bone" },
  { value: "system", label: "Auto" },
  { value: "dark", label: "Graphite" },
] satisfies { value: Theme; label: string }[];

function applyTheme(theme: Theme) {
  const root = document.documentElement;
  const colorway = colorwayOf(theme);
  if (colorway) root.dataset.muColorway = colorway;
  else delete root.dataset.muColorway;
  document.cookie = `${THEME_COOKIE}=${theme}; path=/; max-age=31536000; samesite=lax`;
}

/** Bone / auto / graphite, as a compact switcher. The choice is stored in a cookie so the server renders it without a flash. */
export function ThemeToggle({ initial }: { initial: Theme }) {
  const [theme, setTheme] = useState<Theme>(initial);
  return (
    <Switcher<Theme>
      aria-label="Colorway"
      size="compact"
      className="w-fit"
      value={theme}
      onValueChange={(next) => {
        setTheme(next);
        applyTheme(next);
      }}
      options={options}
    />
  );
}

function subscribe(onChange: () => void) {
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
  const observer = new MutationObserver(onChange);
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-mu-colorway"] });
  mq.addEventListener("change", onChange);
  return () => {
    observer.disconnect();
    mq.removeEventListener("change", onChange);
  };
}

function isDark() {
  const forced = document.documentElement.dataset.muColorway;
  return forced ? forced === "graphite" : window.matchMedia("(prefers-color-scheme: dark)").matches;
}

/** Whether the page is currently in the graphite colorway, honoring the switcher. */
export function useDarkMode() {
  return useSyncExternalStore(subscribe, isDark, () => false);
}