All files / src/components ThemeToggle.tsx

53.84% Statements 7/13
37.5% Branches 3/8
66.66% Functions 2/3
53.84% Lines 7/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49          4x 4x     4x     26x   26x   26x                                 26x                              
"use client";
 
import { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";
 
const THEME_KEY = "theme";
const THEME_CHANGE_EVENT = "theme-change";
 
/** Broadcast theme changes across components in the same tab. */
export const themeChangeTarget = new EventTarget();
 
export default function ThemeToggle() {
  const [isDark, setIsDark] = useState(false);
 
  useEffect(() => {
    // Sync initial state from html class (set by inline script in layout.tsx)
    setIsDark(document.documentElement.classList.contains("dark"));
  }, []);
 
  function toggle() {
    const next = !isDark;
    setIsDark(next);
    document.documentElement.classList.toggle("dark", next);
    try {
      localStorage.setItem(THEME_KEY, next ? "dark" : "light");
    } catch (_e) {
      // storage blocked
    }
    themeChangeTarget.dispatchEvent(
      new CustomEvent(THEME_CHANGE_EVENT, { detail: { dark: next } }),
    );
  }
 
  return (
    <button
      onClick={toggle}
      aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
      title={isDark ? "Switch to light mode" : "Switch to dark mode"}
      className="p-2 rounded-md touch-target-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-2)] transition-colors"
    >
      {isDark ? (
        <Sun size={18} aria-hidden="true" />
      ) : (
        <Moon size={18} aria-hidden="true" />
      )}
    </button>
  );
}