import { useState, useEffect, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { COLOR_PRESETS, ColorPreset } from '../data/colorPresets' import { hexToHsl, hslToHex, isValidHex, HSLColor } from '../utils/colorConversion' import { ThemeColors, DEFAULT_THEME_COLORS } from '../types/theme' import { applyThemeColors } from '../hooks/useThemeColors' interface ThemeBentoPickerProps { currentColors: ThemeColors onColorsChange: (colors: ThemeColors) => void onSave: () => void isSaving: boolean } const CheckIcon = () => ( ) const ChevronDownIcon = () => ( ) const MoonIcon = () => ( ) const SunIcon = () => ( ) const StatusIcon = () => ( ) const PaletteIcon = () => ( ) function PresetCard({ preset, isSelected, onClick, }: { preset: ColorPreset isSelected: boolean onClick: () => void }) { const { i18n } = useTranslation() const isRu = i18n.language === 'ru' return ( ) } function HSLSlider({ label, value, onChange, max, gradient, suffix = '', }: { label: string value: number onChange: (value: number) => void max: number gradient: string suffix?: string }) { return (
{value} {suffix}
onChange(parseInt(e.target.value))} className="w-full h-2.5 rounded-full appearance-none cursor-pointer" style={{ background: gradient }} />
) } function CompactColorInput({ label, value, onChange, }: { label: string value: string onChange: (color: string) => void }) { const [localValue, setLocalValue] = useState(value) const [isEditing, setIsEditing] = useState(false) useEffect(() => { setLocalValue(value) }, [value]) const handleChange = (newValue: string) => { let formatted = newValue.toUpperCase() if (!formatted.startsWith('#')) { formatted = '#' + formatted } setLocalValue(formatted) if (isValidHex(formatted)) { onChange(formatted) } } const handleBlur = () => { setIsEditing(false) if (!isValidHex(localValue)) { setLocalValue(value) } } return (
)}
) } function CollapsibleSection({ title, icon, isOpen, onToggle, children, badge, }: { title: string icon: React.ReactNode isOpen: boolean onToggle: () => void children: React.ReactNode badge?: string }) { return (
{children}
) } export function ThemeBentoPicker({ currentColors, onColorsChange, onSave, isSaving, }: ThemeBentoPickerProps) { const { t } = useTranslation() const [hsl, setHsl] = useState(() => hexToHsl(currentColors.accent)) const [hexInput, setHexInput] = useState(currentColors.accent) const [hasChanges, setHasChanges] = useState(false) const [isPresetsOpen, setIsPresetsOpen] = useState(false) const [isAccentOpen, setIsAccentOpen] = useState(false) const [isDarkOpen, setIsDarkOpen] = useState(false) const [isLightOpen, setIsLightOpen] = useState(false) const [isStatusOpen, setIsStatusOpen] = useState(false) const selectedPresetId = useMemo(() => { const match = COLOR_PRESETS.find( (p) => p.colors.accent.toLowerCase() === currentColors.accent.toLowerCase() && p.colors.darkBackground.toLowerCase() === currentColors.darkBackground.toLowerCase() && p.colors.lightBackground.toLowerCase() === currentColors.lightBackground.toLowerCase() ) return match?.id ?? null }, [currentColors.accent, currentColors.darkBackground, currentColors.lightBackground]) useEffect(() => { setHsl(hexToHsl(currentColors.accent)) setHexInput(currentColors.accent) }, [currentColors.accent]) const updateColor = useCallback( (key: keyof ThemeColors, value: string) => { const newColors = { ...currentColors, [key]: value } onColorsChange(newColors) applyThemeColors(newColors) setHasChanges(true) }, [currentColors, onColorsChange] ) const updateAccentFromHsl = useCallback( (newHsl: HSLColor) => { setHsl(newHsl) const newHex = hslToHex(newHsl.h, newHsl.s, newHsl.l) setHexInput(newHex) updateColor('accent', newHex) }, [updateColor] ) const handleHexInputChange = (value: string) => { setHexInput(value) if (isValidHex(value)) { const newHsl = hexToHsl(value) setHsl(newHsl) updateColor('accent', value) } } const handlePresetSelect = (preset: ColorPreset) => { onColorsChange(preset.colors) applyThemeColors(preset.colors) setHasChanges(true) } const hueGradient = useMemo(() => { return `linear-gradient(to right, hsl(0, ${hsl.s}%, ${hsl.l}%), hsl(60, ${hsl.s}%, ${hsl.l}%), hsl(120, ${hsl.s}%, ${hsl.l}%), hsl(180, ${hsl.s}%, ${hsl.l}%), hsl(240, ${hsl.s}%, ${hsl.l}%), hsl(300, ${hsl.s}%, ${hsl.l}%), hsl(360, ${hsl.s}%, ${hsl.l}%) )` }, [hsl.s, hsl.l]) const saturationGradient = useMemo(() => { return `linear-gradient(to right, hsl(${hsl.h}, 0%, ${hsl.l}%), hsl(${hsl.h}, 100%, ${hsl.l}%) )` }, [hsl.h, hsl.l]) const lightnessGradient = useMemo(() => { return `linear-gradient(to right, hsl(${hsl.h}, ${hsl.s}%, 0%), hsl(${hsl.h}, ${hsl.s}%, 50%), hsl(${hsl.h}, ${hsl.s}%, 100%) )` }, [hsl.h, hsl.s]) return (
} badge={`${COLOR_PRESETS.length}`} isOpen={isPresetsOpen} onToggle={() => setIsPresetsOpen(!isPresetsOpen)} >
{COLOR_PRESETS.map((preset, index) => (
handlePresetSelect(preset)} />
))}

{t('admin.theme.customizeColors', 'Customize Colors')}

} badge={hexInput.toUpperCase()} isOpen={isAccentOpen} onToggle={() => setIsAccentOpen(!isAccentOpen)} >
{hexInput.toUpperCase()}
updateAccentFromHsl({ ...hsl, h })} max={360} gradient={hueGradient} suffix="°" /> updateAccentFromHsl({ ...hsl, s })} max={100} gradient={saturationGradient} suffix="%" /> updateAccentFromHsl({ ...hsl, l })} max={100} gradient={lightnessGradient} suffix="%" />
handleHexInputChange(e.target.value)} placeholder="#3b82f6" maxLength={7} className="input w-full text-sm font-mono uppercase" />
} isOpen={isDarkOpen} onToggle={() => setIsDarkOpen(!isDarkOpen)} >
updateColor('darkBackground', c)} /> updateColor('darkSurface', c)} /> updateColor('darkText', c)} /> updateColor('darkTextSecondary', c)} />
} isOpen={isLightOpen} onToggle={() => setIsLightOpen(!isLightOpen)} >
updateColor('lightBackground', c)} /> updateColor('lightSurface', c)} /> updateColor('lightText', c)} /> updateColor('lightTextSecondary', c)} />
} isOpen={isStatusOpen} onToggle={() => setIsStatusOpen(!isStatusOpen)} >
updateColor('success', c)} /> updateColor('warning', c)} /> updateColor('error', c)} />

{t('theme.preview', 'Preview')}

{t('theme.success', 'Success')} {t('theme.warning', 'Warning')} {t('theme.error', 'Error')}
{hasChanges && (
)}
) }