809 lines
44 KiB
TypeScript
809 lines
44 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTheme, COLOR_PALETTES } from '../../contexts/ThemeContext';
|
|
import type { AccentColor, BorderRadius, SidebarStyle, Density, FontFamily, ColorPalette } from '../../contexts/ThemeContext';
|
|
import { useTranslation } from '../../contexts/LanguageContext';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { useSidebar } from '../../contexts/SidebarContext';
|
|
import type { SidebarMode } from '../../contexts/SidebarContext';
|
|
import { ChromePicker, HuePicker } from 'react-color';
|
|
import '../../styles/ThemeSettings.css';
|
|
|
|
type ThemeTab = 'colors' | 'appearance' | 'preview' | 'advanced';
|
|
|
|
type ColorPickerState = {
|
|
isOpen: boolean;
|
|
theme: 'light' | 'dark';
|
|
property: string;
|
|
value: string;
|
|
} | null;
|
|
|
|
export default function ThemeSettings() {
|
|
const [activeTab, setActiveTab] = useState<ThemeTab>('colors');
|
|
const {
|
|
accentColor,
|
|
borderRadius,
|
|
sidebarStyle,
|
|
density,
|
|
fontFamily,
|
|
colorPalette,
|
|
setAccentColor,
|
|
setBorderRadius,
|
|
setSidebarStyle,
|
|
setDensity,
|
|
setFontFamily,
|
|
setColorPalette,
|
|
saveThemeToBackend
|
|
} = useTheme();
|
|
const { t } = useTranslation();
|
|
const { user } = useAuth();
|
|
const { toggleMobileMenu, sidebarMode, setSidebarMode } = useSidebar();
|
|
const isAdmin = user?.is_superuser || false;
|
|
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
|
text: '',
|
|
left: 0,
|
|
visible: false,
|
|
});
|
|
|
|
const handleTabMouseEnter = (text: string, e: React.MouseEvent) => {
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
setTooltip({
|
|
text,
|
|
left: rect.left + rect.width / 2,
|
|
visible: true,
|
|
});
|
|
};
|
|
|
|
const handleTabMouseLeave = () => {
|
|
setTooltip((prev) => ({ ...prev, visible: false }));
|
|
};
|
|
|
|
// Handlers that save to backend after setting
|
|
const handleAccentColorChange = async (color: AccentColor) => {
|
|
setAccentColor(color);
|
|
saveThemeToBackend({ accentColor: color }).catch(console.error);
|
|
};
|
|
|
|
const handleBorderRadiusChange = async (radius: BorderRadius) => {
|
|
setBorderRadius(radius);
|
|
saveThemeToBackend({ borderRadius: radius }).catch(console.error);
|
|
};
|
|
|
|
const handleSidebarStyleChange = async (style: SidebarStyle) => {
|
|
setSidebarStyle(style);
|
|
saveThemeToBackend({ sidebarStyle: style }).catch(console.error);
|
|
};
|
|
|
|
const handleDensityChange = async (d: Density) => {
|
|
setDensity(d);
|
|
saveThemeToBackend({ density: d }).catch(console.error);
|
|
};
|
|
|
|
const handleFontFamilyChange = async (font: FontFamily) => {
|
|
setFontFamily(font);
|
|
saveThemeToBackend({ fontFamily: font }).catch(console.error);
|
|
};
|
|
|
|
const handleColorPaletteChange = async (palette: ColorPalette) => {
|
|
setColorPalette(palette);
|
|
saveThemeToBackend({ colorPalette: palette }).catch(console.error);
|
|
};
|
|
|
|
// Advanced color states
|
|
const [customColors, setCustomColors] = useState({
|
|
light: {
|
|
bgMain: '#ffffff',
|
|
bgCard: '#f9fafb',
|
|
bgElevated: '#ffffff',
|
|
textPrimary: '#111827',
|
|
textSecondary: '#6b7280',
|
|
border: '#e5e7eb',
|
|
sidebarBg: '#1f2937',
|
|
sidebarText: '#f9fafb'
|
|
},
|
|
dark: {
|
|
bgMain: '#0f172a',
|
|
bgCard: '#1e293b',
|
|
bgElevated: '#334155',
|
|
textPrimary: '#f1f5f9',
|
|
textSecondary: '#94a3b8',
|
|
border: '#334155',
|
|
sidebarBg: '#0c1222',
|
|
sidebarText: '#f9fafb'
|
|
}
|
|
});
|
|
|
|
// Color picker popup state
|
|
const [colorPickerState, setColorPickerState] = useState<ColorPickerState>(null);
|
|
|
|
const colors: { id: AccentColor; label: string; value: string; description: string }[] = [
|
|
{ id: 'auto', label: t.theme.colors.auto, value: '#374151', description: t.theme.colors.autoDesc },
|
|
{ id: 'blue', label: t.theme.colors.blue, value: '#3b82f6', description: t.theme.colors.blueDesc },
|
|
{ id: 'purple', label: t.theme.colors.purple, value: '#8b5cf6', description: t.theme.colors.purpleDesc },
|
|
{ id: 'green', label: t.theme.colors.green, value: '#10b981', description: t.theme.colors.greenDesc },
|
|
{ id: 'orange', label: t.theme.colors.orange, value: '#f97316', description: t.theme.colors.orangeDesc },
|
|
{ id: 'pink', label: t.theme.colors.pink, value: '#ec4899', description: t.theme.colors.pinkDesc },
|
|
{ id: 'red', label: t.theme.colors.red, value: '#ef4444', description: t.theme.colors.redDesc },
|
|
{ id: 'teal', label: t.theme.colors.teal, value: '#14b8a6', description: t.theme.colors.tealDesc },
|
|
{ id: 'amber', label: t.theme.colors.amber, value: '#f59e0b', description: t.theme.colors.amberDesc },
|
|
{ id: 'indigo', label: t.theme.colors.indigo, value: '#6366f1', description: t.theme.colors.indigoDesc },
|
|
{ id: 'cyan', label: t.theme.colors.cyan, value: '#06b6d4', description: t.theme.colors.cyanDesc },
|
|
{ id: 'rose', label: t.theme.colors.rose, value: '#f43f5e', description: t.theme.colors.roseDesc },
|
|
];
|
|
|
|
const radii: { id: BorderRadius; label: string; description: string; preview: string }[] = [
|
|
{ id: 'large', label: t.theme.radius.large, description: t.theme.radius.largeDesc, preview: '16px' },
|
|
{ id: 'medium', label: t.theme.radius.medium, description: t.theme.radius.mediumDesc, preview: '8px' },
|
|
{ id: 'small', label: t.theme.radius.small, description: t.theme.radius.smallDesc, preview: '4px' },
|
|
];
|
|
|
|
const sidebarStyles: { id: SidebarStyle; label: string; description: string }[] = [
|
|
{ id: 'default', label: t.theme.sidebarOptions.default, description: t.theme.sidebarOptions.defaultDesc },
|
|
{ id: 'dark', label: t.theme.sidebarOptions.dark, description: t.theme.sidebarOptions.darkDesc },
|
|
{ id: 'light', label: t.theme.sidebarOptions.light, description: t.theme.sidebarOptions.lightDesc },
|
|
];
|
|
|
|
const sidebarModes: { id: SidebarMode; label: string; description: string }[] = [
|
|
{ id: 'toggle', label: t.admin.sidebarModeToggle, description: t.admin.sidebarModeToggleDesc },
|
|
{ id: 'dynamic', label: t.admin.sidebarModeDynamic, description: t.admin.sidebarModeDynamicDesc },
|
|
{ id: 'collapsed', label: t.admin.sidebarModeCollapsed, description: t.admin.sidebarModeCollapsedDesc },
|
|
];
|
|
|
|
const densities: { id: Density; label: string; description: string }[] = [
|
|
{ id: 'compact', label: t.theme.densityOptions.compact, description: t.theme.densityOptions.compactDesc },
|
|
{ id: 'comfortable', label: t.theme.densityOptions.comfortable, description: t.theme.densityOptions.comfortableDesc },
|
|
{ id: 'spacious', label: t.theme.densityOptions.spacious, description: t.theme.densityOptions.spaciousDesc },
|
|
];
|
|
|
|
const fonts: { id: FontFamily; label: string; description: string; fontStyle: string }[] = [
|
|
{ id: 'sans', label: t.theme.fontOptions.system, description: t.theme.fontOptions.systemDesc, fontStyle: 'system-ui' },
|
|
{ id: 'inter', label: t.theme.fontOptions.inter, description: t.theme.fontOptions.interDesc, fontStyle: 'Inter' },
|
|
{ id: 'roboto', label: t.theme.fontOptions.roboto, description: t.theme.fontOptions.robotoDesc, fontStyle: 'Roboto' },
|
|
];
|
|
|
|
const palettes: { id: ColorPalette; label: string; description: string }[] = [
|
|
{ id: 'monochrome', label: t.theme.palettes.monochrome, description: t.theme.palettes.monochromeDesc },
|
|
{ id: 'default', label: t.theme.palettes.default, description: t.theme.palettes.defaultDesc },
|
|
{ id: 'monochromeBlue', label: t.theme.palettes.monochromeBlue, description: t.theme.palettes.monochromeBlueDesc },
|
|
{ id: 'sepia', label: t.theme.palettes.sepia, description: t.theme.palettes.sepiaDesc },
|
|
{ id: 'nord', label: t.theme.palettes.nord, description: t.theme.palettes.nordDesc },
|
|
{ id: 'dracula', label: t.theme.palettes.dracula, description: t.theme.palettes.draculaDesc },
|
|
{ id: 'solarized', label: t.theme.palettes.solarized, description: t.theme.palettes.solarizedDesc },
|
|
{ id: 'github', label: t.theme.palettes.github, description: t.theme.palettes.githubDesc },
|
|
{ id: 'ocean', label: t.theme.palettes.ocean, description: t.theme.palettes.oceanDesc },
|
|
{ id: 'forest', label: t.theme.palettes.forest, description: t.theme.palettes.forestDesc },
|
|
{ id: 'midnight', label: t.theme.palettes.midnight, description: t.theme.palettes.midnightDesc },
|
|
{ id: 'sunset', label: t.theme.palettes.sunset, description: t.theme.palettes.sunsetDesc },
|
|
];
|
|
|
|
// Helper component for color control
|
|
const ColorControl = ({ theme, property, label, value }: {
|
|
theme: 'light' | 'dark';
|
|
property: string;
|
|
label: string;
|
|
value: string
|
|
}) => {
|
|
return (
|
|
<div className="color-control-item">
|
|
<label className="color-control-label">
|
|
<span>{label}</span>
|
|
<span className="color-value-display selectable">{value}</span>
|
|
</label>
|
|
<div className="color-control-actions">
|
|
<div className="color-input-group">
|
|
<button
|
|
className="btn-color-picker"
|
|
style={{ backgroundColor: value }}
|
|
title={t.theme.pickColor}
|
|
onClick={() => {
|
|
setColorPickerState({
|
|
isOpen: true,
|
|
theme,
|
|
property,
|
|
value
|
|
});
|
|
}}
|
|
>
|
|
{/* Color preview via background color */}
|
|
</button>
|
|
<input
|
|
type="text"
|
|
value={value.toUpperCase()}
|
|
onChange={(e) => handleHexInput(theme, property, e.target.value)}
|
|
className="color-hex-input"
|
|
placeholder="#FFFFFF"
|
|
maxLength={7}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Color picker handlers
|
|
const handleColorChange = (theme: 'light' | 'dark', property: string, value: string) => {
|
|
setCustomColors(prev => ({
|
|
...prev,
|
|
[theme]: {
|
|
...prev[theme],
|
|
[property]: value
|
|
}
|
|
}));
|
|
// Apply to CSS variables immediately
|
|
const root = document.documentElement;
|
|
const varName = `--color-${property.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
|
root.style.setProperty(varName, value);
|
|
};
|
|
|
|
const handleHexInput = (theme: 'light' | 'dark', property: string, value: string) => {
|
|
// Validate hex color format
|
|
const hexRegex = /^#?[0-9A-Fa-f]{6}$/;
|
|
const formattedValue = value.startsWith('#') ? value : `#${value}`;
|
|
|
|
if (hexRegex.test(formattedValue)) {
|
|
handleColorChange(theme, property, formattedValue);
|
|
}
|
|
};
|
|
|
|
const resetToDefaults = () => {
|
|
setCustomColors({
|
|
light: {
|
|
bgMain: '#ffffff',
|
|
bgCard: '#f9fafb',
|
|
bgElevated: '#ffffff',
|
|
textPrimary: '#111827',
|
|
textSecondary: '#6b7280',
|
|
border: '#e5e7eb',
|
|
sidebarBg: '#1f2937',
|
|
sidebarText: '#f9fafb'
|
|
},
|
|
dark: {
|
|
bgMain: '#0f172a',
|
|
bgCard: '#1e293b',
|
|
bgElevated: '#334155',
|
|
textPrimary: '#f1f5f9',
|
|
textSecondary: '#94a3b8',
|
|
border: '#334155',
|
|
sidebarBg: '#0c1222',
|
|
sidebarText: '#f9fafb'
|
|
}
|
|
});
|
|
// Reset CSS variables
|
|
const root = document.documentElement;
|
|
root.style.removeProperty('--color-bg-main');
|
|
root.style.removeProperty('--color-bg-card');
|
|
root.style.removeProperty('--color-bg-elevated');
|
|
root.style.removeProperty('--color-text-primary');
|
|
root.style.removeProperty('--color-text-secondary');
|
|
root.style.removeProperty('--color-border');
|
|
root.style.removeProperty('--color-sidebar-bg');
|
|
root.style.removeProperty('--color-sidebar-text');
|
|
};
|
|
|
|
return (
|
|
<main className="main-content theme-settings-root">
|
|
{/* Tab Tooltip */}
|
|
{tooltip.visible && (
|
|
<div
|
|
className="admin-tab-tooltip"
|
|
style={{ left: tooltip.left }}
|
|
>
|
|
{tooltip.text}
|
|
</div>
|
|
)}
|
|
{/* Modern Tab Navigation */}
|
|
<div className="page-tabs-container">
|
|
<div className="page-tabs-slider">
|
|
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
|
<span className="material-symbols-outlined">menu</span>
|
|
</button>
|
|
<div className="page-title-section">
|
|
<span className="material-symbols-outlined">brush</span>
|
|
<span className="page-title-text">{t.theme.title}</span>
|
|
</div>
|
|
<div className="admin-tabs-divider"></div>
|
|
<button
|
|
className={`admin-tab-btn ${activeTab === 'colors' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('colors')}
|
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.colorsTab, e)}
|
|
onMouseLeave={handleTabMouseLeave}
|
|
>
|
|
<span className="material-symbols-outlined">color_lens</span>
|
|
<span>{t.theme.colorsTab}</span>
|
|
</button>
|
|
<button
|
|
className={`admin-tab-btn ${activeTab === 'appearance' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('appearance')}
|
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.appearanceTab, e)}
|
|
onMouseLeave={handleTabMouseLeave}
|
|
>
|
|
<span className="material-symbols-outlined">tune</span>
|
|
<span>{t.theme.appearanceTab}</span>
|
|
</button>
|
|
<button
|
|
className={`admin-tab-btn ${activeTab === 'preview' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('preview')}
|
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.previewTab, e)}
|
|
onMouseLeave={handleTabMouseLeave}
|
|
>
|
|
<span className="material-symbols-outlined">visibility</span>
|
|
<span>{t.theme.previewTab}</span>
|
|
</button>
|
|
{isAdmin && (
|
|
<button
|
|
className={`admin-tab-btn ${activeTab === 'advanced' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('advanced')}
|
|
onMouseEnter={(e) => handleTabMouseEnter(t.theme.advancedTab, e)}
|
|
onMouseLeave={handleTabMouseLeave}
|
|
>
|
|
<span className="material-symbols-outlined">code</span>
|
|
<span>{t.theme.advancedTab}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-tab-content">
|
|
{activeTab === 'colors' && (
|
|
<div className="theme-tab-content">
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.accentColor}</h3>
|
|
</div>
|
|
<div className="color-grid-enhanced">
|
|
{colors.map((color) => (
|
|
<div
|
|
key={color.id}
|
|
className={`color-card ${accentColor === color.id ? 'active' : ''}`}
|
|
onClick={() => handleAccentColorChange(color.id)}
|
|
>
|
|
<div className="color-swatch-large" style={{ backgroundColor: color.value }}>
|
|
{accentColor === color.id && (
|
|
<span className="material-symbols-outlined">check</span>
|
|
)}
|
|
</div>
|
|
<div className="color-info">
|
|
<span className="color-name">{color.label}</span>
|
|
<span className="color-description">{color.description}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.colorPalette}</h3>
|
|
</div>
|
|
<div className="palette-grid">
|
|
{palettes.map((palette) => {
|
|
const paletteColors = COLOR_PALETTES[palette.id];
|
|
return (
|
|
<div
|
|
key={palette.id}
|
|
className={`palette-card ${colorPalette === palette.id ? 'active' : ''}`}
|
|
onClick={() => handleColorPaletteChange(palette.id)}
|
|
>
|
|
<div className="palette-preview">
|
|
<div className="palette-swatch-row">
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.bgMain }} title="Light BG" />
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.bgCard }} title="Light Card" />
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.textPrimary }} title="Light Text" />
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.sidebarBg }} title="Light Sidebar" />
|
|
</div>
|
|
<div className="palette-swatch-row">
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.bgMain }} title="Dark BG" />
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.bgCard }} title="Dark Card" />
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.textPrimary }} title="Dark Text" />
|
|
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.sidebarBg }} title="Dark Sidebar" />
|
|
</div>
|
|
{colorPalette === palette.id && (
|
|
<div className="palette-check">
|
|
<span className="material-symbols-outlined">check</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="palette-info">
|
|
<span className="palette-name">{palette.label}</span>
|
|
<span className="palette-description">{palette.description}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'appearance' && (
|
|
<div className="theme-tab-content">
|
|
<div className="appearance-grid">
|
|
{/* Border Radius Section */}
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.borderRadius}</h3>
|
|
</div>
|
|
<div className="option-cards">
|
|
{radii.map((radius) => (
|
|
<div
|
|
key={radius.id}
|
|
className={`option-card ${borderRadius === radius.id ? 'active' : ''}`}
|
|
onClick={() => handleBorderRadiusChange(radius.id)}
|
|
>
|
|
<div className="option-preview">
|
|
<div
|
|
className="radius-preview-box"
|
|
style={{ borderRadius: radius.preview }}
|
|
></div>
|
|
</div>
|
|
<div className="option-info">
|
|
<span className="option-name">{radius.label}</span>
|
|
<span className="option-description">{radius.description}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar Style Section */}
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.sidebarStyle}</h3>
|
|
</div>
|
|
<div className="option-cards">
|
|
{sidebarStyles.map((style) => (
|
|
<div
|
|
key={style.id}
|
|
className={`option-card ${sidebarStyle === style.id ? 'active' : ''}`}
|
|
onClick={() => handleSidebarStyleChange(style.id)}
|
|
>
|
|
<div className="option-preview">
|
|
<div className={`sidebar-preview sidebar-preview-${style.id}`}>
|
|
<div className="sidebar-part"></div>
|
|
<div className="content-part"></div>
|
|
</div>
|
|
</div>
|
|
<div className="option-info">
|
|
<span className="option-name">{style.label}</span>
|
|
<span className="option-description">{style.description}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar Mode Section */}
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.admin.sidebarMode}</h3>
|
|
</div>
|
|
<div className="option-cards">
|
|
{sidebarModes.map((mode) => (
|
|
<div
|
|
key={mode.id}
|
|
className={`option-card ${sidebarMode === mode.id ? 'active' : ''}`}
|
|
onClick={() => setSidebarMode(mode.id)}
|
|
>
|
|
<div className="option-preview">
|
|
<div className={`sidebar-mode-preview sidebar-mode-${mode.id}`}>
|
|
<div className="sidebar-line"></div>
|
|
<div className="sidebar-line"></div>
|
|
</div>
|
|
</div>
|
|
<div className="option-info">
|
|
<span className="option-name">{mode.label}</span>
|
|
<span className="option-description">{mode.description}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Density Section */}
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.density}</h3>
|
|
</div>
|
|
<div className="option-cards">
|
|
{densities.map((d) => (
|
|
<div
|
|
key={d.id}
|
|
className={`option-card ${density === d.id ? 'active' : ''}`}
|
|
onClick={() => handleDensityChange(d.id)}
|
|
>
|
|
<div className="option-preview">
|
|
<div className={`density-preview density-preview-${d.id}`}>
|
|
<div className="density-line"></div>
|
|
<div className="density-line"></div>
|
|
<div className="density-line"></div>
|
|
</div>
|
|
</div>
|
|
<div className="option-info">
|
|
<span className="option-name">{d.label}</span>
|
|
<span className="option-description">{d.description}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Font Family Section */}
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.fontFamily}</h3>
|
|
</div>
|
|
<div className="option-cards">
|
|
{fonts.map((f) => (
|
|
<div
|
|
key={f.id}
|
|
className={`option-card ${fontFamily === f.id ? 'active' : ''}`}
|
|
onClick={() => handleFontFamilyChange(f.id)}
|
|
>
|
|
<div className="option-preview">
|
|
<div className="font-preview" style={{ fontFamily: f.fontStyle }}>
|
|
Aa
|
|
</div>
|
|
</div>
|
|
<div className="option-info">
|
|
<span className="option-name">{f.label}</span>
|
|
<span className="option-description">{f.description}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'preview' && (
|
|
<div className="theme-tab-content">
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.preview}</h3>
|
|
</div>
|
|
<div className="preview-container">
|
|
<div className="preview-card">
|
|
<div className="preview-header">
|
|
<h3>{t.theme.previewCard}</h3>
|
|
<span className="badge badge-accent">{t.theme.badge}</span>
|
|
</div>
|
|
<p>{t.theme.previewDescription}</p>
|
|
<div className="preview-actions">
|
|
<button className="btn-primary">{t.theme.primaryButton}</button>
|
|
<button className="btn-ghost">{t.theme.ghostButton}</button>
|
|
</div>
|
|
<div className="preview-inputs">
|
|
<input type="text" placeholder={t.theme.inputPlaceholder} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="preview-card">
|
|
<div className="preview-header">
|
|
<h3>{t.theme.sampleCard}</h3>
|
|
<span className="badge badge-success">{t.dashboard.active}</span>
|
|
</div>
|
|
<p>{t.theme.sampleCardDesc}</p>
|
|
<div className="preview-stats">
|
|
<div className="stat-item">
|
|
<span className="stat-value">142</span>
|
|
<span className="stat-label">{t.theme.totalItems}</span>
|
|
</div>
|
|
<div className="stat-item">
|
|
<span className="stat-value">89%</span>
|
|
<span className="stat-label">{t.theme.successRate}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'advanced' && isAdmin && (
|
|
<div className="theme-tab-content">
|
|
<div className="theme-section">
|
|
<div className="section-header">
|
|
<h3 className="section-title">{t.theme.advancedColors}</h3>
|
|
<button className="btn-ghost" onClick={resetToDefaults} style={{ marginTop: '1rem' }}>
|
|
<span className="material-symbols-outlined">refresh</span>
|
|
{t.theme.resetColors}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="advanced-colors-grid" style={{ marginTop: '2rem' }}>
|
|
{/* Light Theme Colors */}
|
|
<div className="color-theme-section">
|
|
<h3 className="color-theme-title">{t.theme.lightThemeColors}</h3>
|
|
<div className="color-controls-list">
|
|
<ColorControl
|
|
theme="light"
|
|
property="bgMain"
|
|
label={t.theme.background}
|
|
value={customColors.light.bgMain}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="bgCard"
|
|
label={t.theme.backgroundCard}
|
|
value={customColors.light.bgCard}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="bgElevated"
|
|
label={t.theme.backgroundElevated}
|
|
value={customColors.light.bgElevated}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="textPrimary"
|
|
label={t.theme.textPrimary}
|
|
value={customColors.light.textPrimary}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="textSecondary"
|
|
label={t.theme.textSecondary}
|
|
value={customColors.light.textSecondary}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="border"
|
|
label={t.theme.border}
|
|
value={customColors.light.border}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="sidebarBg"
|
|
label={t.theme.sidebarBackground}
|
|
value={customColors.light.sidebarBg}
|
|
/>
|
|
<ColorControl
|
|
theme="light"
|
|
property="sidebarText"
|
|
label={t.theme.sidebarText}
|
|
value={customColors.light.sidebarText}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dark Theme Colors */}
|
|
<div className="color-theme-section">
|
|
<h3 className="color-theme-title">{t.theme.darkThemeColors}</h3>
|
|
<div className="color-controls-list">
|
|
<ColorControl
|
|
theme="dark"
|
|
property="bgMain"
|
|
label={t.theme.background}
|
|
value={customColors.dark.bgMain}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="bgCard"
|
|
label={t.theme.backgroundCard}
|
|
value={customColors.dark.bgCard}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="bgElevated"
|
|
label={t.theme.backgroundElevated}
|
|
value={customColors.dark.bgElevated}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="textPrimary"
|
|
label={t.theme.textPrimary}
|
|
value={customColors.dark.textPrimary}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="textSecondary"
|
|
label={t.theme.textSecondary}
|
|
value={customColors.dark.textSecondary}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="border"
|
|
label={t.theme.border}
|
|
value={customColors.dark.border}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="sidebarBg"
|
|
label={t.theme.sidebarBackground}
|
|
value={customColors.dark.sidebarBg}
|
|
/>
|
|
<ColorControl
|
|
theme="dark"
|
|
property="sidebarText"
|
|
label={t.theme.sidebarText}
|
|
value={customColors.dark.sidebarText}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Color Picker Popup */}
|
|
{colorPickerState && (
|
|
<div
|
|
className="color-picker-overlay"
|
|
onClick={() => setColorPickerState(null)}
|
|
>
|
|
<div
|
|
className="color-picker-popup"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="color-picker-header">
|
|
<h3>{t.theme.pickColor}</h3>
|
|
<button
|
|
className="btn-close-picker"
|
|
onClick={() => setColorPickerState(null)}
|
|
>
|
|
<span className="material-symbols-outlined">close</span>
|
|
</button>
|
|
</div>
|
|
<div className="color-picker-content">
|
|
<div className="color-picker-preview-section">
|
|
<div
|
|
className="color-preview-box"
|
|
style={{ backgroundColor: colorPickerState.value }}
|
|
/>
|
|
<div className="color-preview-info">
|
|
<span className="color-preview-hex">{colorPickerState.value.toUpperCase()}</span>
|
|
<span className="color-preview-label">{t.theme.currentColor}</span>
|
|
</div>
|
|
</div>
|
|
<div className="chrome-picker-wrapper">
|
|
<ChromePicker
|
|
color={colorPickerState.value}
|
|
onChange={(color) => {
|
|
handleColorChange(
|
|
colorPickerState.theme,
|
|
colorPickerState.property,
|
|
color.hex
|
|
);
|
|
setColorPickerState({
|
|
...colorPickerState,
|
|
value: color.hex
|
|
});
|
|
}}
|
|
disableAlpha={true}
|
|
/>
|
|
<div className="hue-picker-wrapper">
|
|
<HuePicker
|
|
color={colorPickerState.value}
|
|
onChange={(color) => {
|
|
handleColorChange(
|
|
colorPickerState.theme,
|
|
colorPickerState.property,
|
|
color.hex
|
|
);
|
|
setColorPickerState({
|
|
...colorPickerState,
|
|
value: color.hex
|
|
});
|
|
}}
|
|
width="100%"
|
|
height="16px"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="color-picker-actions">
|
|
<button
|
|
className="btn-primary btn-full-width"
|
|
onClick={() => setColorPickerState(null)}
|
|
>
|
|
{t.theme.apply}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|