Repository Public MIT
1"use client";2import { Monitor, Moon, Sun } from "lucide-react";3import { useEffect, useState } from "react";45type Mode = "system" | "light" | "dark";6const ORDER: Mode[] = ["system", "light", "dark"];7const LABEL: Record<Mode, string> = { system: "System theme", light: "Light theme", dark: "Dark theme" };89function apply(mode: Mode) {10 const dark = mode === "dark" || (mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);11 document.documentElement.dataset.theme = dark ? "dark" : "light";12 document.querySelector('meta[name="theme-color"]')?.setAttribute("content", dark ? "#080b10" : "#f6f7f9");13}1415/** Cycles system → light → dark. The choice is a display preference only, kept in localStorage. */16export function ThemeToggle() {17 const [mode, setMode] = useState<Mode>("system");18 useEffect(() => {19 const saved = localStorage.getItem("skills-theme");20 if (saved === "light" || saved === "dark") setMode(saved);21 const mq = window.matchMedia("(prefers-color-scheme: dark)");22 const onChange = () => { if ((localStorage.getItem("skills-theme") ?? "system") === "system") apply("system"); };23 mq.addEventListener("change", onChange);24 return () => mq.removeEventListener("change", onChange);25 }, []);26 const Icon = mode === "light" ? Sun : mode === "dark" ? Moon : Monitor;27 const next = ORDER[(ORDER.indexOf(mode) + 1) % ORDER.length]!;28 return (29 <button30 type="button"31 onClick={() => {32 setMode(next);33 if (next === "system") localStorage.removeItem("skills-theme"); else localStorage.setItem("skills-theme", next);34 apply(next);35 }}36 className="grid h-9 w-9 place-items-center rounded-lg border border-line text-muted transition-colors hover:border-line-strong hover:text-text"37 aria-label={`${LABEL[mode]}. Switch to ${LABEL[next].toLowerCase()}`}38 title={`${LABEL[mode]} — click for ${LABEL[next].toLowerCase()}`}39 data-testid="theme-toggle"40 data-mode={mode}41 >42 <Icon size={15} aria-hidden />43 </button>44 );45}46