All files / components/layout theme-switcher.tsx

100% Statements 16/16
96% Branches 24/25
100% Functions 5/5
100% Lines 16/16

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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 1311x                                   6x 6x   6x 3x       6x 6x   6x 3x               3x 1x           1x       3x     1x                                                     2x     2x                                                                                                
"use client";
 
import { useTheme } from "next-themes";
import { motion } from "framer-motion";
import { Sun, Moon, Monitor } from "lucide-react";
import { useEffect, useState } from "react";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
 
interface ThemeSwitcherProps {
  variant?: "default" | "toggle";
}
 
export function ThemeSwitcher({ variant = "default" }: ThemeSwitcherProps) {
  const { theme, setTheme, systemTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
 
  useEffect(() => {
    setMounted(true);
  }, []);
 
  // Handle current theme in toggle mode
  const currentTheme = theme === "system" ? systemTheme : theme;
  const isDark = currentTheme === "dark";
 
  if (!mounted) {
    return variant === "default" ? (
      <div className="flex items-center gap-2 p-1 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse w-[100px] h-8" />
    ) : (
      <div className="w-16 h-8 bg-gray-300 dark:bg-gray-800 rounded-full" />
    );
  }
 
  // ---------- VARIANT 1: DEFAULT TOOLTIP STYLE ----------
  if (variant === "default") {
    const themes = [
      { mode: "light", icon: <Sun className="w-4 h-4" />, label: "Light" },
      { mode: "dark", icon: <Moon className="w-4 h-4" />, label: "Dark" },
      { mode: "system", icon: <Monitor className="w-4 h-4" />, label: "System" },
    ];
 
    return (
      <TooltipProvider>
        <div className="flex items-center p-0.5 bg-gray-100 dark:bg-gray-900 rounded-lg">
          {themes.map(({ mode, icon, label }) => (
            <Tooltip key={mode}>
              <TooltipTrigger asChild>
                <motion.button
                  onClick={() => setTheme(mode)}
                  aria-label={`Switch to ${label} theme`}
                  title={`Switch to ${label} theme`}
                  className={`flex items-center justify-center w-8 h-8 rounded-md transition-colors duration-200
                    ${
                      theme === mode
                        ? "bg-gray-300 dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm"
                        : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
                    }
                  `}
                  whileHover={{ scale: 1.05 }}
                  whileTap={{ scale: 0.95 }}
                >
                  {icon}
                </motion.button>
              </TooltipTrigger>
              <TooltipContent>
                <p>{label}</p>
              </TooltipContent>
            </Tooltip>
          ))}
        </div>
      </TooltipProvider>
    );
  }
 
  // ---------- VARIANT 2: TOGGLE STYLE ----------
  return (
    <div className="flex items-center gap-3">
      <motion.button
        onClick={() => setTheme(isDark ? "light" : "dark")}
        aria-label={
          isDark
            ? "Switch to light theme"
            : "Switch to dark theme"
        }
        title={
          isDark
            ? "Switch to light theme"
            : "Switch to dark theme"
        }
        className="relative w-16 h-8 rounded-full bg-gray-300 dark:bg-gray-800 flex items-center px-1 shadow-lg border border-gray-400 dark:border-gray-700"
        whileTap={{ scale: 0.95 }}
        animate={{ scale: 1 }}
        transition={{ type: "spring", stiffness: 300, damping: 20 }}
      >
        <motion.div
          className="w-7 h-7 bg-white dark:bg-gray-950 rounded-full shadow-xl flex items-center justify-center"
          initial={{ x: isDark ? 32 : 0 }}
          animate={{ x: isDark ? 32 : 0 }}
          transition={{
            type: "spring",
            stiffness: 250,
            damping: 20,
            mass: 0.5,
          }}
          whileHover={{ scale: 1.1 }}
          whileTap={{ scale: 0.9, rotate: 15 }}
        >
          <motion.div
            key={currentTheme}
            initial={{ rotate: isDark ? -180 : 180, opacity: 0 }}
            animate={{ rotate: 0, opacity: 1 }}
            transition={{ type: "spring", stiffness: 200, damping: 20 }}
          >
            {isDark ? (
              <Moon className="w-4 h-4 text-gray-300" />
            ) : (
              <Sun className="w-4 h-4 text-orange-500" />
            )}
          </motion.div>
        </motion.div>
      </motion.button>
    </div>
  );
}
 
export default ThemeSwitcher;