- Migrated from Python pre-commit to NodeJS-native solution - Reorganized documentation structure - Set up Husky + lint-staged for efficient pre-commit hooks - Fixed Dockerfile healthcheck issue - Added comprehensive documentation index
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { useTheme } from '../../hooks/useTheme';
|
|
import { SunIcon, MoonIcon, DesktopIcon } from '../icons/Icons';
|
|
|
|
type Theme = 'light' | 'dark' | 'system';
|
|
|
|
const themeOptions: {
|
|
value: Theme;
|
|
label: string;
|
|
icon: React.FC<React.ComponentProps<'svg'>>;
|
|
}[] = [
|
|
{ value: 'light', label: 'Light', icon: SunIcon },
|
|
{ value: 'dark', label: 'Dark', icon: MoonIcon },
|
|
{ value: 'system', label: 'System', icon: DesktopIcon },
|
|
];
|
|
|
|
const ThemeSwitcher: React.FC = () => {
|
|
const { theme, setTheme } = useTheme();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const currentTheme =
|
|
themeOptions.find(t => t.value === theme) || themeOptions[2];
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
return (
|
|
<div className='relative' ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className='flex items-center justify-center w-10 h-10 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors'
|
|
aria-label={`Current theme: ${currentTheme.label}. Change theme.`}
|
|
>
|
|
<SunIcon className='w-5 h-5 text-slate-700 dark:hidden' />
|
|
<MoonIcon className='w-5 h-5 text-slate-200 hidden dark:block' />
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className='absolute right-0 mt-2 w-36 bg-white dark:bg-slate-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 py-1 z-30 border dark:border-slate-700'>
|
|
{themeOptions.map(option => (
|
|
<button
|
|
key={option.value}
|
|
onClick={() => {
|
|
setTheme(option.value);
|
|
setIsOpen(false);
|
|
}}
|
|
className={`w-full text-left flex items-center space-x-2 px-3 py-2 text-sm ${
|
|
theme === option.value
|
|
? 'bg-indigo-600 text-white'
|
|
: 'text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
}`}
|
|
>
|
|
<option.icon className='w-4 h-4' />
|
|
<span>{option.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ThemeSwitcher;
|