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:
2025-12-15 18:14:47 +01:00
parent 04a0fe4b27
commit ba53e0eff0
31 changed files with 4277 additions and 374 deletions

View File

@@ -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>