didi-lot3-frontend/web/src/components/ThemeToggle/ThemeToggle.tsx
Top Clossers cec967f953 Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser
- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3)
- Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor
- Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS
- Configurare adresă platformă externalizată (build args / .env / config.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:40:40 +03:00

239 lines
6.3 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import { useTheme } from '../../contexts/ThemeContext';
import { spacing, borderRadius, animation, sizing } from '../../theme';
// SVG Icons (inline for no external dependencies)
const SunIcon: React.FC<{ size?: number }> = ({ size = 20 }) => (
<svg
width={size}
height={size}
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" />
</svg>
);
const MoonIcon: React.FC<{ size?: number }> = ({ size = 20 }) => (
<svg
width={size}
height={size}
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" />
</svg>
);
const SystemIcon: React.FC<{ size?: number }> = ({ size = 20 }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
);
type ThemeMode = 'dark' | 'light' | 'system';
const themeOptionDefs: { value: ThemeMode; labelKey: string; icon: React.ReactNode }[] = [
{ value: 'dark', labelKey: 'theme.dark', icon: <MoonIcon size={16} /> },
{ value: 'light', labelKey: 'theme.light', icon: <SunIcon size={16} /> },
{ value: 'system', labelKey: 'theme.system', icon: <SystemIcon size={16} /> },
];
export const ThemeToggle: React.FC = () => {
const { t } = useTranslation();
const { theme, setTheme, resolvedTheme } = useTheme();
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// Close on escape key
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen]);
// Determine which icon to show based on theme state
const getCurrentIcon = () => {
if (theme === 'system') {
return <SystemIcon />;
}
return resolvedTheme === 'dark' ? <MoonIcon /> : <SunIcon />;
};
const handleSelect = (value: ThemeMode) => {
setTheme(value);
setIsOpen(false);
};
return (
<ToggleContainer ref={containerRef}>
<ToggleButton
onClick={() => setIsOpen(!isOpen)}
aria-label={t('theme.toggleMenu')}
aria-expanded={isOpen}
aria-haspopup="listbox"
title={t('theme.currentTheme', { theme: theme === 'system' ? t('theme.system') : resolvedTheme })}
>
{getCurrentIcon()}
</ToggleButton>
{isOpen && (
<Dropdown role="listbox" aria-label={t('theme.selectTheme')}>
{themeOptionDefs.map((option) => (
<DropdownItem
key={option.value}
onClick={() => handleSelect(option.value)}
role="option"
aria-selected={theme === option.value}
isActive={theme === option.value}
>
<IconWrapper>{option.icon}</IconWrapper>
<span>{t(option.labelKey)}</span>
</DropdownItem>
))}
</Dropdown>
)}
</ToggleContainer>
);
};
const ToggleContainer = styled.div`
position: relative;
display: inline-block;
`;
const ToggleButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: ${sizing.button.lg}px;
height: ${sizing.button.lg}px;
background: var(--bg-surface);
border: 1px solid var(--border-default);
border-radius: ${borderRadius.md}px;
cursor: pointer;
color: var(--fg-primary);
transition: all ${animation.duration.normal} ${animation.easing.default};
&:hover {
background: var(--accent-subtle);
border-color: var(--accent-border);
transform: translateY(-2px);
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(0, 145, 152, 0.3);
}
&:active {
transform: translateY(0);
}
svg {
flex-shrink: 0;
}
`;
const Dropdown = styled.div`
position: absolute;
top: calc(100% + ${spacing.sm}px);
right: 0;
background: var(--bg-elevated);
border: 1px solid var(--border-subtle);
border-radius: ${borderRadius.sm}px;
padding: ${spacing.xs}px;
min-width: 140px;
z-index: 1000;
box-shadow: var(--shadow-lg);
`;
const DropdownItem = styled.button<{ isActive: boolean }>`
display: flex;
align-items: center;
gap: ${spacing.sm}px;
width: 100%;
padding: ${spacing.sm}px ${spacing.md}px;
background: ${props => props.isActive ? 'var(--accent-subtle)' : 'transparent'};
border: none;
border-radius: ${borderRadius.sm}px;
cursor: pointer;
color: ${props => props.isActive ? 'var(--accent-text)' : 'var(--fg-primary)'};
font-size: 14px;
text-align: left;
transition: background ${animation.duration.fast} ${animation.easing.default};
&:hover {
background: ${props => props.isActive ? 'var(--accent-subtle)' : 'var(--bg-hover)'};
}
&:focus {
outline: none;
background: var(--accent-subtle);
}
`;
const IconWrapper = styled.span`
display: flex;
align-items: center;
justify-content: center;
width: ${sizing.icon.sm}px;
height: ${sizing.icon.sm}px;
`;