83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
'use client'
|
|
import { useEffect, useState } from 'react'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
|
|
export type Theme = 'dark' | 'light'
|
|
|
|
const STORAGE_KEY = 'ghost-theme'
|
|
|
|
export function useTheme(): [Theme, () => void] {
|
|
const [theme, setTheme] = useState<Theme>('dark')
|
|
|
|
useEffect(() => {
|
|
const stored = (localStorage.getItem(STORAGE_KEY) as Theme | null) ?? 'dark'
|
|
setTheme(stored)
|
|
document.documentElement.setAttribute('data-theme', stored)
|
|
}, [])
|
|
|
|
const toggle = () => {
|
|
const next: Theme = theme === 'dark' ? 'light' : 'dark'
|
|
setTheme(next)
|
|
document.documentElement.setAttribute('data-theme', next)
|
|
localStorage.setItem(STORAGE_KEY, next)
|
|
}
|
|
|
|
return [theme, toggle]
|
|
}
|
|
|
|
export default function ThemeToggle() {
|
|
const [theme, toggle] = useTheme()
|
|
const isLight = theme === 'light'
|
|
|
|
return (
|
|
<motion.button
|
|
onClick={toggle}
|
|
whileHover={{ scale: 1.08 }}
|
|
whileTap={{ scale: 0.90 }}
|
|
className="relative g-btn h-8 w-8 px-0 flex items-center justify-center overflow-hidden"
|
|
title={`Switch to ${isLight ? 'dark' : 'light'} mode`}
|
|
aria-label={`Switch to ${isLight ? 'dark' : 'light'} mode`}
|
|
>
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
{isLight ? (
|
|
/* Moon — switch to dark */
|
|
<motion.svg
|
|
key="moon"
|
|
initial={{ rotate: -90, opacity: 0, scale: 0.5 }}
|
|
animate={{ rotate: 0, opacity: 1, scale: 1 }}
|
|
exit={{ rotate: 90, opacity: 0, scale: 0.5 }}
|
|
transition={{ duration: 0.2 }}
|
|
width="14" height="14" viewBox="0 0 24 24"
|
|
fill="none" stroke="currentColor" strokeWidth="2"
|
|
strokeLinecap="round" strokeLinejoin="round"
|
|
>
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
</motion.svg>
|
|
) : (
|
|
/* Sun — switch to light */
|
|
<motion.svg
|
|
key="sun"
|
|
initial={{ rotate: 90, opacity: 0, scale: 0.5 }}
|
|
animate={{ rotate: 0, opacity: 1, scale: 1 }}
|
|
exit={{ rotate: -90, opacity: 0, scale: 0.5 }}
|
|
transition={{ duration: 0.2 }}
|
|
width="14" height="14" viewBox="0 0 24 24"
|
|
fill="none" stroke="currentColor" strokeWidth="2"
|
|
strokeLinecap="round" strokeLinejoin="round"
|
|
>
|
|
<circle cx="12" cy="12" r="5" />
|
|
<line x1="12" y1="1" x2="12" y2="3" />
|
|
<line x1="12" y1="21" x2="12" y2="23" />
|
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
<line x1="1" y1="12" x2="3" y2="12" />
|
|
<line x1="21" y1="12" x2="23" y2="12" />
|
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
</motion.svg>
|
|
)}
|
|
</AnimatePresence>
|
|
</motion.button>
|
|
)
|
|
}
|