Refactor settings system and improve context initialization
Backend: - Add type validation and coercion for settings API - Implement SettingStorage and SettingType in registry - Improve CRUD operations for settings Frontend: - Refactor Theme, Language, Sidebar, ViewMode contexts - Simplify admin components (GeneralTab, SettingsTab, UsersTab) - Add new settings endpoints to API client - Improve App initialization flow Infrastructure: - Update Dockerfile and docker-compose.yml - Add .dockerignore - Update Makefile and README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, 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';
|
||||
@@ -10,10 +10,13 @@ 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: 'light' | 'dark';
|
||||
property: string;
|
||||
theme: ThemeMode;
|
||||
property: ColorProperty;
|
||||
value: string;
|
||||
} | null;
|
||||
|
||||
@@ -32,6 +35,8 @@ export default function ThemeSettings() {
|
||||
setDensity,
|
||||
setFontFamily,
|
||||
setColorPalette,
|
||||
customColors,
|
||||
setCustomColors,
|
||||
saveThemeToBackend
|
||||
} = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -88,32 +93,25 @@ export default function ThemeSettings() {
|
||||
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 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 },
|
||||
@@ -177,8 +175,8 @@ export default function ThemeSettings() {
|
||||
|
||||
// Helper component for color control
|
||||
const ColorControl = ({ theme, property, label, value }: {
|
||||
theme: 'light' | 'dark';
|
||||
property: string;
|
||||
theme: ThemeMode;
|
||||
property: ColorProperty;
|
||||
label: string;
|
||||
value: string
|
||||
}) => {
|
||||
@@ -220,21 +218,35 @@ export default function ThemeSettings() {
|
||||
};
|
||||
|
||||
// Color picker handlers
|
||||
const handleColorChange = (theme: 'light' | 'dark', property: string, value: string) => {
|
||||
setCustomColors(prev => ({
|
||||
...prev,
|
||||
[theme]: {
|
||||
...prev[theme],
|
||||
[property]: value
|
||||
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;
|
||||
}
|
||||
}));
|
||||
// Apply to CSS variables immediately
|
||||
const root = document.documentElement;
|
||||
const varName = `--color-${property.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
||||
root.style.setProperty(varName, value);
|
||||
|
||||
if (Object.keys(themeOverrides).length === 0) {
|
||||
delete next[theme];
|
||||
} else {
|
||||
next[theme] = themeOverrides;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleHexInput = (theme: 'light' | 'dark', property: string, value: string) => {
|
||||
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}`;
|
||||
@@ -245,38 +257,9 @@ export default function ThemeSettings() {
|
||||
};
|
||||
|
||||
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');
|
||||
if (!isAdmin) return;
|
||||
hasUserModifiedCustomColors.current = true;
|
||||
setCustomColors({});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -619,49 +602,49 @@ export default function ThemeSettings() {
|
||||
theme="light"
|
||||
property="bgMain"
|
||||
label={t.theme.background}
|
||||
value={customColors.light.bgMain}
|
||||
value={effectiveColors.light.bgMain}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="bgCard"
|
||||
label={t.theme.backgroundCard}
|
||||
value={customColors.light.bgCard}
|
||||
value={effectiveColors.light.bgCard}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="bgElevated"
|
||||
label={t.theme.backgroundElevated}
|
||||
value={customColors.light.bgElevated}
|
||||
value={effectiveColors.light.bgElevated}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="textPrimary"
|
||||
label={t.theme.textPrimary}
|
||||
value={customColors.light.textPrimary}
|
||||
value={effectiveColors.light.textPrimary}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="textSecondary"
|
||||
label={t.theme.textSecondary}
|
||||
value={customColors.light.textSecondary}
|
||||
value={effectiveColors.light.textSecondary}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="border"
|
||||
label={t.theme.border}
|
||||
value={customColors.light.border}
|
||||
value={effectiveColors.light.border}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="sidebarBg"
|
||||
label={t.theme.sidebarBackground}
|
||||
value={customColors.light.sidebarBg}
|
||||
value={effectiveColors.light.sidebarBg}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="light"
|
||||
property="sidebarText"
|
||||
label={t.theme.sidebarText}
|
||||
value={customColors.light.sidebarText}
|
||||
value={effectiveColors.light.sidebarText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -674,49 +657,49 @@ export default function ThemeSettings() {
|
||||
theme="dark"
|
||||
property="bgMain"
|
||||
label={t.theme.background}
|
||||
value={customColors.dark.bgMain}
|
||||
value={effectiveColors.dark.bgMain}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="bgCard"
|
||||
label={t.theme.backgroundCard}
|
||||
value={customColors.dark.bgCard}
|
||||
value={effectiveColors.dark.bgCard}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="bgElevated"
|
||||
label={t.theme.backgroundElevated}
|
||||
value={customColors.dark.bgElevated}
|
||||
value={effectiveColors.dark.bgElevated}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="textPrimary"
|
||||
label={t.theme.textPrimary}
|
||||
value={customColors.dark.textPrimary}
|
||||
value={effectiveColors.dark.textPrimary}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="textSecondary"
|
||||
label={t.theme.textSecondary}
|
||||
value={customColors.dark.textSecondary}
|
||||
value={effectiveColors.dark.textSecondary}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="border"
|
||||
label={t.theme.border}
|
||||
value={customColors.dark.border}
|
||||
value={effectiveColors.dark.border}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="sidebarBg"
|
||||
label={t.theme.sidebarBackground}
|
||||
value={customColors.dark.sidebarBg}
|
||||
value={effectiveColors.dark.sidebarBg}
|
||||
/>
|
||||
<ColorControl
|
||||
theme="dark"
|
||||
property="sidebarText"
|
||||
label={t.theme.sidebarText}
|
||||
value={customColors.dark.sidebarText}
|
||||
value={effectiveColors.dark.sidebarText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user