Files
app-service/frontend/src/pages/admin/ThemeSettings.tsx
matteoscrugli 02c14e3fbd Add tab bar position setting with edge swipe sidebar
- Add theme_tab_bar_position setting (top/bottom/responsive)
- Tab bar is now fixed at top, stays visible during scroll
- Bottom position uses fixed positioning with safe-area-inset
- Add edge swipe gesture to open sidebar on mobile
- Remove backdrop-filter for better scroll performance
- Simplify TabsScroller by removing inline style manipulation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 02:05:49 +01:00

849 lines
46 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useTheme, COLOR_PALETTES } from '../../contexts/ThemeContext';
import type { AccentColor, BorderRadius, SidebarStyle, Density, FontFamily, ColorPalette, TabBarPosition } 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 { SwipeTabs } from '../../components/SwipeTabs';
import TabsScroller from '../../components/TabsScroller';
import '../../styles/ThemeSettings.css';
type ThemeTab = 'colors' | 'appearance' | 'preview' | 'advanced';
type ThemeMode = 'light' | 'dark';
type ColorProperty = keyof typeof COLOR_PALETTES.default.light;
type ColorPickerState = {
isOpen: boolean;
theme: ThemeMode;
property: ColorProperty;
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,
customColors,
setCustomColors,
tabBarPosition,
setTabBarPosition,
saveThemeToBackend
} = useTheme();
const { t } = useTranslation();
const { user } = useAuth();
const { toggleMobileMenu, sidebarMode, setSidebarMode } = useSidebar();
const isAdmin = user?.is_superuser || false;
const tabs: ThemeTab[] = isAdmin ? ['colors', 'appearance', 'preview', 'advanced'] : ['colors', 'appearance', 'preview'];
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);
};
const handleTabBarPositionChange = async (position: TabBarPosition) => {
setTabBarPosition(position);
saveThemeToBackend({ tabBarPosition: position }).catch(console.error);
};
// Color picker popup state
const [colorPickerState, setColorPickerState] = useState<ColorPickerState>(null);
const hasUserModifiedCustomColors = useRef(false);
const paletteColors = COLOR_PALETTES[colorPalette];
const effectiveColors = {
light: { ...paletteColors.light, ...(customColors.light ?? {}) },
dark: { ...paletteColors.dark, ...(customColors.dark ?? {}) },
};
useEffect(() => {
if (!isAdmin || !hasUserModifiedCustomColors.current) return;
const timeoutId = setTimeout(() => {
saveThemeToBackend({ customColors }).catch(console.error);
}, 300);
return () => clearTimeout(timeoutId);
}, [customColors, isAdmin, saveThemeToBackend]);
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 tabBarPositions: { id: TabBarPosition; label: string; description: string }[] = [
{ id: 'top', label: t.theme.tabBarPositions?.top || 'Sopra', description: t.theme.tabBarPositions?.topDesc || 'Barra dei tab sempre in alto' },
{ id: 'bottom', label: t.theme.tabBarPositions?.bottom || 'Sotto', description: t.theme.tabBarPositions?.bottomDesc || 'Barra dei tab sempre in basso' },
{ id: 'responsive', label: t.theme.tabBarPositions?.responsive || 'Responsivo', description: t.theme.tabBarPositions?.responsiveDesc || 'In alto su desktop, in basso su mobile' },
];
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: ThemeMode;
property: ColorProperty;
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: ThemeMode, property: ColorProperty, value: string) => {
if (!isAdmin) return;
hasUserModifiedCustomColors.current = true;
const baseValue = paletteColors[theme][property];
const normalizedValue = value.trim().toLowerCase();
const normalizedBase = baseValue.trim().toLowerCase();
setCustomColors((prev) => {
const next = { ...prev };
const themeOverrides = { ...(prev[theme] ?? {}) } as Record<string, string>;
if (normalizedValue === normalizedBase) {
delete themeOverrides[property];
} else {
themeOverrides[property] = value;
}
if (Object.keys(themeOverrides).length === 0) {
delete next[theme];
} else {
next[theme] = themeOverrides;
}
return next;
});
};
const handleHexInput = (theme: ThemeMode, property: ColorProperty, 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 = () => {
if (!isAdmin) return;
hasUserModifiedCustomColors.current = true;
setCustomColors({});
};
const renderPanel = (tab: ThemeTab) => {
switch (tab) {
case 'colors':
return (
<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>
);
case 'appearance':
return (
<div className="theme-tab-content">
<div className="appearance-grid">
<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>
<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>
<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>
<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>
<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 className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.tabBarPosition || 'Posizione Tab'}</h3>
</div>
<div className="option-cards">
{tabBarPositions.map((pos) => (
<div
key={pos.id}
role="button"
tabIndex={0}
className={`option-card ${tabBarPosition === pos.id ? 'active' : ''}`}
onClick={() => handleTabBarPositionChange(pos.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleTabBarPositionChange(pos.id);
}
}}
>
<div className="option-preview">
<div className={`tabbar-preview tabbar-preview-${pos.id}`}>
<div className="tabbar-line"></div>
<div className="tabbar-content"></div>
</div>
</div>
<div className="option-info">
<span className="option-name">{pos.label}</span>
<span className="option-description">{pos.description}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
case 'preview':
return (
<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>
);
case 'advanced':
if (!isAdmin) return null;
return (
<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' }}>
<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={effectiveColors.light.bgMain}
/>
<ColorControl
theme="light"
property="bgCard"
label={t.theme.backgroundCard}
value={effectiveColors.light.bgCard}
/>
<ColorControl
theme="light"
property="bgElevated"
label={t.theme.backgroundElevated}
value={effectiveColors.light.bgElevated}
/>
<ColorControl
theme="light"
property="textPrimary"
label={t.theme.textPrimary}
value={effectiveColors.light.textPrimary}
/>
<ColorControl
theme="light"
property="textSecondary"
label={t.theme.textSecondary}
value={effectiveColors.light.textSecondary}
/>
<ColorControl
theme="light"
property="border"
label={t.theme.border}
value={effectiveColors.light.border}
/>
<ColorControl
theme="light"
property="sidebarBg"
label={t.theme.sidebarBackground}
value={effectiveColors.light.sidebarBg}
/>
<ColorControl
theme="light"
property="sidebarText"
label={t.theme.sidebarText}
value={effectiveColors.light.sidebarText}
/>
</div>
</div>
<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={effectiveColors.dark.bgMain}
/>
<ColorControl
theme="dark"
property="bgCard"
label={t.theme.backgroundCard}
value={effectiveColors.dark.bgCard}
/>
<ColorControl
theme="dark"
property="bgElevated"
label={t.theme.backgroundElevated}
value={effectiveColors.dark.bgElevated}
/>
<ColorControl
theme="dark"
property="textPrimary"
label={t.theme.textPrimary}
value={effectiveColors.dark.textPrimary}
/>
<ColorControl
theme="dark"
property="textSecondary"
label={t.theme.textSecondary}
value={effectiveColors.dark.textSecondary}
/>
<ColorControl
theme="dark"
property="border"
label={t.theme.border}
value={effectiveColors.dark.border}
/>
<ColorControl
theme="dark"
property="sidebarBg"
label={t.theme.sidebarBackground}
value={effectiveColors.dark.sidebarBg}
/>
<ColorControl
theme="dark"
property="sidebarText"
label={t.theme.sidebarText}
value={effectiveColors.dark.sidebarText}
/>
</div>
</div>
</div>
</div>
</div>
);
default:
return null;
}
};
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">
<TabsScroller className="page-tabs-slider" showArrows>
<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="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>
)}
</TabsScroller>
</div>
<SwipeTabs
className="admin-tab-swipe"
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
renderPanel={renderPanel}
panelClassName="admin-tab-content swipe-panel-content"
/>
{/* 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>
);
}