"use client"; import { useEffect, useRef, useState, type ComponentProps, type KeyboardEvent, type SVGProps, } from "react"; import { useTheme } from "fumadocs-ui/provider/base"; /** * Three-icon theme switcher (light / system / dark) rendered as a radio group — * each icon selects its own theme, unlike fumadocs' default "light-dark" * switcher, which is a single blind toggle that flips on any click. Reads * `theme`, not `resolvedTheme`, so the "system" option can show as selected * (resolvedTheme collapses system to light/dark). Dropped into the sidebar * footer pill via `slots.themeSwitch`, so fumadocs passes the container * className (left divider, `ms-auto`, rounded inner buttons); we merge it onto * our own base. * * Icons are inlined (the project doesn't depend on `lucide-react` directly); * `useTheme` is re-exported by fumadocs so we avoid a bare `next-themes` import. */ function SunIcon(props: SVGProps) { return ( ); } function MonitorIcon(props: SVGProps) { return ( ); } function MoonIcon(props: SVGProps) { return ( ); } const OPTIONS = [ ["light", SunIcon], ["system", MonitorIcon], ["dark", MoonIcon], ] as const; function cx(...classes: (string | false | undefined)[]): string { return classes.filter(Boolean).join(" "); } export function ThemeToggle({ className, ...props }: ComponentProps<"div">) { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); const active = mounted ? theme : null; const buttonsRef = useRef<(HTMLButtonElement | null)[]>([]); // Pre-mount nothing is selected, so keep the first control tabbable. const selectedIndex = OPTIONS.findIndex(([key]) => key === active); const rovingIndex = selectedIndex === -1 ? 0 : selectedIndex; // Radio-group keyboard model: arrows move focus and pick that theme. function onKeyDown(event: KeyboardEvent, index: number) { const delta = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 0; if (delta === 0) return; event.preventDefault(); const next = (index + delta + OPTIONS.length) % OPTIONS.length; setTheme(OPTIONS[next][0]); buttonsRef.current[next]?.focus(); } return (
{OPTIONS.map(([key, Icon], index) => ( ))}
); }