Phase 0 Complete: Tasks 0.7-0.20

- 0.7: UI-Design-Richtlinien (docs/ui-design-guidelines.md, 535 lines)
- 0.8: Theme-Customization Backend (4 theme fields, migration 0023)
- 0.9: Theme-Customization Frontend (SettingsTheme.tsx, themeStore.ts, live preview)
- 0.10: RBAC-Audit (4 plugins secured, 53 routes with require_permission)
- 0.11: LiteLLM-Cleanup (llm_client.py migrated from httpx to litellm)
- 0.12: KI-Agent-Framework docs (plugin-development-guide.md, agent_capabilities field)
- 0.13: Heartbeat configurable (ProactiveSettings, migration 0024, frontend UI)
- 0.14: Unified Search Field-Level RBAC (resolve_permissions + filter_fields_by_permission)
- 0.15: Undo/History-System (EntityHistory model, service, routes, migration 0025, HistoryViewer)
- 0.16: Storage Backend (LocalStorage + S3Storage, DMS/attachments/mail updated)
- 0.17: Import/Export unified Contact fields (firstname, surname, email_1, phone_1)
- 0.18: .gitignore & Config-Cleanup (webui→frontend, python-jose removed, .env untracked)
- 0.19: Mail-Salt Security-Fix (per-account random salt, migration 0026)
- 0.20: AGPL replaced (PyMuPDF→pypdf, OnlyOffice→Collabora, LICENSE + THIRD_PARTY_LICENSES.md)
This commit is contained in:
Agent Zero
2026-07-23 08:42:26 +02:00
parent 3d06cb2353
commit ec81940178
65 changed files with 3061 additions and 277 deletions
+153
View File
@@ -0,0 +1,153 @@
/**
* Theme store — manages dynamic CSS variables for theme customization.
* Applies primary_color, accent_color, font_family, border_radius
* from system settings to CSS custom properties on :root.
*/
import { create } from 'zustand';
export interface ThemeConfig {
primaryColor: string;
accentColor: string;
fontFamily: string;
borderRadius: string;
darkMode: boolean;
}
interface ThemeState extends ThemeConfig {
setTheme: (config: Partial<ThemeConfig>) => void;
toggleDarkMode: () => void;
applyTheme: () => void;
loadFromStorage: () => void;
saveToStorage: () => void;
}
const DEFAULT_THEME: ThemeConfig = {
primaryColor: '#2563eb',
accentColor: '#d946ef',
fontFamily: 'Inter',
borderRadius: '0.5rem',
darkMode: false,
};
/** Convert hex color to RGB values for Tailwind CSS variable injection */
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
/** Generate a Tailwind-style color scale (50-900) from a base hex color */
function generateColorScale(hex: string): Record<string, string> {
const rgb = hexToRgb(hex);
if (!rgb) return {};
// Mix with white for lighter shades, with black for darker
const mix = (base: number, amount: number, target: number) =>
Math.round(base + (target - base) * amount);
const scales: Record<number, [number, number]> = {
50: [0.95, 0], // 95% white
100: [0.90, 0],
200: [0.80, 0],
300: [0.60, 0],
400: [0.30, 0],
500: [0, 0], // base color
600: [0, 0.10], // 10% black
700: [0, 0.20],
800: [0, 0.30],
900: [0, 0.40],
};
const result: Record<string, string> = {};
for (const [scale, [whiteAmt, blackAmt]] of Object.entries(scales)) {
const r = whiteAmt > 0 ? mix(rgb.r, whiteAmt, 255) : mix(rgb.r, blackAmt, 0);
const g = whiteAmt > 0 ? mix(rgb.g, whiteAmt, 255) : mix(rgb.g, blackAmt, 0);
const b = whiteAmt > 0 ? mix(rgb.b, whiteAmt, 255) : mix(rgb.b, blackAmt, 0);
result[scale] = `rgb(${r} ${g} ${b})`;
}
result.DEFAULT = hex;
return result;
}
function applyCSSVariables(config: ThemeConfig) {
const root = document.documentElement;
// Apply primary color scale
const primaryScale = generateColorScale(config.primaryColor);
for (const [key, val] of Object.entries(primaryScale)) {
root.style.setProperty(`--color-primary-${key.toLowerCase()}`, val);
}
// Apply accent color scale
const accentScale = generateColorScale(config.accentColor);
for (const [key, val] of Object.entries(accentScale)) {
root.style.setProperty(`--color-accent-${key.toLowerCase()}`, val);
}
// Apply font family
root.style.setProperty('--font-sans', `'${config.fontFamily}', system-ui, sans-serif`);
// Apply border radius
root.style.setProperty('--radius-md', config.borderRadius);
// Apply dark mode
if (config.darkMode) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}
const STORAGE_KEY = 'leocrm-theme';
export const useThemeStore = create<ThemeState>((set, get) => ({
...DEFAULT_THEME,
setTheme: (config) => {
set((state) => ({ ...state, ...config }));
get().applyTheme();
get().saveToStorage();
},
toggleDarkMode: () => {
set((state) => ({ ...state, darkMode: !state.darkMode }));
get().applyTheme();
get().saveToStorage();
},
applyTheme: () => {
const state = get();
applyCSSVariables({
primaryColor: state.primaryColor,
accentColor: state.accentColor,
fontFamily: state.fontFamily,
borderRadius: state.borderRadius,
darkMode: state.darkMode,
});
},
loadFromStorage: () => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const config = JSON.parse(stored) as Partial<ThemeConfig>;
set((state) => ({ ...state, ...config }));
}
} catch {
// ignore parse errors
}
get().applyTheme();
},
saveToStorage: () => {
const state = get();
const { setTheme, toggleDarkMode, applyTheme, loadFromStorage, saveToStorage, ...config } = state;
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
},
}));