Add theming system: custom palettes, per-user colour mode, admin appearance page

- 4 built-in themes (Default, Pastel, High Contrast, Ocean Blue) seeded as
  JSON files in /config/themes/ on startup; custom themes can be created,
  edited, and deleted via the new admin Appearance page
- All theme tokens applied via JS inline CSS properties (no hardcoded CSS blocks)
- New `color_mode` column on users table (migration dd6ad2f2c211); users can
  override the admin-set global default in Settings
- Backend: GET/PATCH /settings/appearance, full CRUD on /settings/themes
- Frontend: AdminAppearancePage with theme grid + colour pickers, SettingsPage
  replaces placeholder with mode selector, useTheme rewritten to fetch from API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-18 01:46:17 +02:00
parent da9b911f1e
commit 608b0b7fe8
15 changed files with 1063 additions and 34 deletions
+51
View File
@@ -26,6 +26,7 @@ export interface UserData {
full_name: string | null;
is_active: boolean;
is_admin: boolean;
color_mode: string | null;
}
export const getMe = () => api.get<UserData>("/users/me").then((r) => r.data);
@@ -202,6 +203,56 @@ export const renameCategory = (id: string, name: string) =>
export const deleteCategory = (id: string) =>
api.delete(`/documents/categories/${id}`);
// --- Appearance & Themes ---
export interface ThemeColors {
primary: string;
primary_hover: string;
accent: string;
accent_hover: string;
background: string;
surface: string;
border: string;
text_primary: string;
text_muted: string;
}
export interface ThemeDefinition {
id: string;
label: string;
builtin: boolean;
light: ThemeColors;
dark: ThemeColors;
}
export interface AppearanceSettings {
theme: string;
default_mode: string;
}
export const getAppearanceSettings = (): Promise<AppearanceSettings> =>
api.get<AppearanceSettings>("/settings/appearance").then((r) => r.data);
export const updateAppearanceSettings = (data: AppearanceSettings): Promise<AppearanceSettings> =>
api.patch<AppearanceSettings>("/settings/appearance", data).then((r) => r.data);
export const getThemes = (): Promise<ThemeDefinition[]> =>
api.get<ThemeDefinition[]>("/settings/themes").then((r) => r.data);
export const createTheme = (data: Omit<ThemeDefinition, "builtin">): Promise<ThemeDefinition> =>
api.post<ThemeDefinition>("/settings/themes", data).then((r) => r.data);
export const updateTheme = (
id: string,
data: { label?: string; light?: ThemeColors; dark?: ThemeColors }
): Promise<ThemeDefinition> =>
api.patch<ThemeDefinition>(`/settings/themes/${id}`, data).then((r) => r.data);
export const deleteTheme = (id: string): Promise<void> =>
api.delete(`/settings/themes/${id}`).then((r) => r.data);
export const updateColorMode = (color_mode: string): Promise<UserData> =>
api.patch<UserData>("/users/me/color-mode", { color_mode }).then((r) => r.data);
// --- Settings (admin only) ---
export interface AIProviderUpdate {
provider: string;