C-ERR-BOUNDARY: ErrorBoundary erweitert (trace_id, Fallback-UI, PluginErrorBoundary) C-NOTIF: NotificationDropdown mit System-Channel-Link C-DOCS: ApiDocs.tsx Seite (Swagger UI iframe) C-A11Y: aria-label, min-h-touch, focus-visible ergänzt C-FE-TEST: 29 neue Tests (ErrorBoundary, ApiDocs, PrintButton, themeStore, NotificationDropdown) C-DOC: ui-design-guidelines.md aktualisiert Verifiziert (keine Änderungen nötig): - C-TAGS, C-CF, C-FILTER, C-DEDUP, C-ONBOARD, C-THEME, C-PWA, C-PRINT TSC: 0 errors, Build: erfolgreich, Vitest: 29/29 passed
22 KiB
LeoCRM UI-Design-Richtlinien
Version: 1.0
Datum: 2026-07-23
Gültig für: Alle Frontend-Komponenten, Plugin-Seiten und zukünftige Entwicklungen
1. Farbsystem
Alle Farben sind als Tailwind Design Tokens in tailwind.config.js definiert. Jede Farbe hat Schattierungen von 50 (hell) bis 900 (dunkel) plus einen DEFAULT-Wert.
| Token | Hex (DEFAULT) | Verwendung |
|---|---|---|
primary |
#2563eb (Blau) |
Hauptaktionen, aktive Zustände, Links, Fokus-Ringe |
secondary |
#64748b (Slate) |
Text, Borders, Hintergründe, inaktive Zustände |
accent |
#d946ef (Fuchsia) |
Hervorhebungen, Info-Badges, KI-Features |
danger |
#dc2626 (Rot) |
Löschen, Fehler, destruktive Aktionen |
warning |
#f59e0b (Amber) |
Warnungen, ausstehende Aktionen |
success |
#16a34a (Grün) |
Erfolg, Bestätigungen, aktive Status |
Verwendungsregeln
- Primary nur für die wichtigste Aktion pro View. Nicht mehr als eine Primary-Button pro Formular.
- Secondary für Text, Borders und inaktive UI-Elemente.
secondary-50für Card-Footer,secondary-100für Hover-Zustände. - Accent sparsam für KI-Features und Hervorhebungen. Nicht für Standard-Aktionen.
- Danger ausschließlich für destruktive Aktionen (Löschen, Entfernen). Immer mit
ConfirmDialogkombinieren. - Warning für Status-Badges und Warnhinweise. Nicht als Button-Farbe.
- Success für Erfolgsmeldungen und Status-Indikatoren. Nicht als Standard-Button.
Dark Mode
- Aktiviert via
darkMode: 'class'in Tailwind Config. - CSS-Variablen in
:root(Light) und.dark(Dark) definiert. - Dark Mode-Toggle in Settings.
- Beim Dark Mode werden
secondary-900als Hintergrund undsecondary-50als Text verwendet.
2. Typografie
| Eigenschaft | Wert |
|---|---|
| Font Family | Inter (system-ui fallback) |
| Mono Font | JetBrains Mono für Code/Daten |
| Rendering | antialiased |
Schriftgrößen-Hierarchie
| Token | Größe | Zeilenhöhe | Verwendung |
|---|---|---|---|
text-xs |
0.75rem | 1rem | Badges, Tooltips, Metadaten |
text-sm |
0.875rem | 1.25rem | Labels, Helper-Text, Tabellen-Spalten |
text-base |
1rem | 1.5rem | Body-Text, Input-Felder |
text-lg |
1.125rem | 1.75rem | Card-Titel, Section-Header |
text-xl |
1.25rem | 1.75rem | Seiten-Titel |
text-2xl |
1.5rem | 2rem | Dashboard-Überschriften |
text-3xl |
1.875rem | 2.25rem | Große Überschriften |
text-4xl |
2.25rem | 2.5rem | Hero-Text, Login-Titel |
Font-Weight
font-medium(500) — Buttons, Labels, Tab-Headerfont-semibold(600) — Card-Titel, Seiten-Titelfont-bold(700) — Nur für Hervorhebungen, sparsam
3. Layout-Patterns
3-Spalten-Explorer-Layout
Standard-Layout für Explorer-Plugins (Calendar, Mail, DMS, Contacts):
┌─────────────┬──────────────────┬──────────────────────┐
│ Tree │ Liste/Explorer │ Detail │
│ (224px) │ (flex-1) │ (flex-1 / 60%) │
│ ResizablePanel│ ResizablePanel │ ResizablePanel │
└─────────────┴──────────────────┴──────────────────────┘
- Linke Spalte:
ResizablePanelmitinitialWidth=224,minWidth=150,maxWidth=600 - Mittlere Spalte:
ResizablePanelmitresizable=false(flex-1) - Rechte Spalte:
ResizablePanelmitresizable=falseoderhandleSide="left" - Drag-Handle auf der rechten Kante der linken Spalte
import { ResizablePanel } from '@/components/ui/ResizablePanel';
<div className="flex h-full">
<ResizablePanel initialWidth={224} minWidth={150} maxWidth={600}>
<TreeView />
</ResizablePanel>
<div className="flex-1 overflow-auto">
<ListView />
</div>
<div className="flex-1 overflow-auto">
<DetailView />
</div>
</div>
PluginToolbar
Jede Plugin-Seite registriert Aktionen über den usePluginToolbarStore:
import { usePluginToolbarStore, type ToolbarItem } from '@/store/pluginToolbarStore';
const { setItems, setActivePlugin } = usePluginToolbarStore();
useEffect(() => {
setActivePlugin('calendar');
setItems([
{ id: 'create', plugin: 'calendar', type: 'button', label: 'Neu', icon: <Plus />, onClick: handleCreate, group: 'actions' },
{ id: 'search', plugin: 'calendar', type: 'search', searchPlaceholder: 'Suchen...', onSearch: handleSearch, group: 'search' },
]);
}, []);
- Toolbar-Items werden nach
groupgruppiert mit Trennern zwischen Gruppen. - Button-Labels sind auf Mobile (
hidden sm:inline) ausgeblendet, Icons bleiben sichtbar. - Toolbar-Höhe:
min-h-[43px], Hintergrundbg-white, Border-Bottomborder-secondary-200.
Modal-Dialoge
Für Formulare, Bestätigungen und Dialoge:
import { Modal } from '@/components/ui/Modal';
<Modal open={open} onClose={onClose} title="Kontakt bearbeiten" size="lg">
<ContactForm />
</Modal>
| Size | max-width | Verwendung |
|---|---|---|
sm |
max-w-md | Bestätigungsdialoge |
md |
max-w-lg | Einfache Formulare |
lg |
max-w-2xl | Komplexe Formulare, Edit-Dialoge |
xl |
max-w-4xl | Große Formulare, Multi-Step |
ConfirmDialogfür destruktive Aktionen (Löschen, Entfernen).- Focus-Trap: Fokus wird beim Öffnen auf erstes fokussierbares Element gesetzt, beim Schließen auf ursprüngliches Element zurückgegeben.
- Escape-Taste schließt Modal (sofern
closeOnEscape=true). - Backdrop-Klick schließt Modal (sofern
closeOnBackdrop=true).
Settings-Layout
Settings-Seiten verwenden einen Tree-Navigator links und das Formular rechts:
┌─────────────┬──────────────────────────────────┐
│ Settings │ Settings-Formular │
│ Tree │ (Cards mit Sections) │
│ (224px) │ (flex-1, scrollable) │
└─────────────┴──────────────────────────────────┘
4. Komponenten-Referenz
Button
import { Button } from '@/components/ui/Button';
<Button variant="primary" size="md" onClick={handleSave} isLoading={saving}>
Speichern
</Button>
| Prop | Typ | Default | Beschreibung |
|---|---|---|---|
variant |
'primary' | 'secondary' | 'danger' | 'ghost' |
'primary' |
Visuelle Variante |
size |
'sm' | 'md' | 'lg' |
'md' |
Größe |
isLoading |
boolean |
false |
Zeigt Spinner, deaktiviert Button |
icon |
ReactNode |
— | Icon links vom Text |
fullWidth |
boolean |
false |
width: 100% |
- Alle Buttons haben
min-h-touch(44px) für Touch-Accessibility. focus-visible:ring-2für Tastatur-Navigation.motion-safe:duration-200für Übergänge (respektiertprefers-reduced-motion).
Card
import { Card } from '@/components/ui/Card';
<Card title="Kontaktdaten" description="Stammdaten" actions={<Button>Edit</Button>}>
<CardContent />
</Card>
| Prop | Typ | Beschreibung |
|---|---|---|
title |
string |
Card-Header-Titel |
description |
string |
Subtitel im Header |
actions |
ReactNode |
Aktionen rechts im Header |
footer |
ReactNode |
Footer-Bereich (bg-secondary-50) |
- Hintergrund:
bg-white, Border:border-secondary-200, Radius:rounded-lg, Shadow:shadow-sm. - Body-Padding:
px-6 py-4, Footer-Padding:px-6 py-3.
Badge
import { Badge } from '@/components/ui/Badge';
<Badge variant="success" dot>Aktiv</Badge>
| Variant | Verwendung |
|---|---|
default |
Neutrale Tags |
primary |
Primäre Zustände |
success |
Aktiv, bestätigt, online |
warning |
Ausstehend, Warnung |
danger |
Fehler, inaktiv, abgelaufen |
info |
Info, KI-Vorschläge |
secondary |
Sekundäre Tags |
dotprop zeigt einen farbigen Punkt links an.- Größe:
text-xs,px-2.5 py-0.5,rounded-full.
Input / Select
import { Input } from '@/components/ui/Input';
<Input label="Name" error={errors.name} helperText="Vollständiger Name" required />
focus:ring-2 focus:ring-primary-500bei Fokus.- Error-State:
border-danger-500,text-danger-900. aria-invalid,aria-describedbyfür Accessibility.min-h-touch(44px) für Touch-Targets.- Label:
text-sm font-medium text-secondary-700.
Table / DataGrid
- Verwendet TanStack Table für Sortierung, Filterung, Pagination.
aria-labelauf sortierbare Headers.- Zebra-Stiping optional:
even:bg-secondary-50.
Toast
import { useToast } from '@/components/ui/Toast';
const { toast } = useToast();
toast({ title: 'Gespeichert', description: 'Kontakt wurde gespeichert', variant: 'success' });
- Wird nach jeder CRUD-Aktion verwendet (Erfolg/Fehler).
- Auto-Dismiss nach 5 Sekunden.
- Position: Top-Right (Desktop), Top (Mobile).
EmptyState
import { EmptyState } from '@/components/ui/EmptyState';
<EmptyState icon={<Users />} title="Keine Kontakte" description="Erstellen Sie einen neuen Kontakt" action={<Button>Neu</Button>} />
- Verwendet wenn Liste leer ist.
- Icon groß zentriert, Titel + Beschreibung, optional Aktion.
Skeleton
import { Skeleton } from '@/components/ui/Skeleton';
<Skeleton className="h-8 w-full" />
- Verwendet während Daten laden.
animate-pulseAnimation.- Respektiert
prefers-reduced-motion.
5. Spacing & Sizing
Padding
| Element | Padding |
|---|---|
| Card Body | px-6 py-4 |
| Card Footer | px-6 py-3 |
| Panel | p-4 |
| Modal Body | p-6 |
| Input | px-3 py-2 |
Gap
| Verwendung | Gap |
|---|---|
| Button-Gruppen | gap-2 |
| Form-Sections | gap-4 |
| Spalten / Panels | gap-6 |
| Toolbar-Items | gap-1 |
Border-Radius
| Token | Wert | Verwendung |
|---|---|---|
rounded-sm |
0.375rem | Badges, kleine Elemente |
rounded-md |
0.5rem | Inputs, Buttons, Panels (Standard) |
rounded-lg |
0.75rem | Cards, Modals |
rounded-xl |
1rem | Große Container |
rounded-full |
9999px | Badges, Avatars |
Shadow
| Token | Verwendung |
|---|---|
shadow-sm |
Cards, Panels |
shadow-md |
Dropdowns, Popovers |
shadow-lg |
Modals, Dialoge |
6. Accessibility
Pflicht-Regeln
- Focus-Ring: Alle interaktiven Elemente haben
focus-visible:ring-2 focus-visible:ring-primary-500. - Touch-Targets: Mindestens 44×44px (
min-h-touch min-w-touch). - ARIA-Labels: Dekorative SVGs erhalten
aria-hidden="true". Interaktive Elemente ohne sichtbaren Text erhaltenaria-label. - Screen Reader:
sr-onlyKlasse für Text nur für Screen Reader.sr-only-focusablefür Skip-Links. - Reduced Motion:
motion-safe:undmotion-reduce:Präfixe verwenden.prefers-reduced-motionMedia Query wird respektiert. - Tastatur-Navigation: Tab-Reihenfolge folgt visueller Reihenfolge. Escape schließt Modals/Dropdowns.
- Farbkontrast: Mindestens 4.5:1 für Body-Text, 3:1 für große Texte und UI-Komponenten.
Implementierte Patterns
focus-ringKlasse:focus-visible:ring-2 focus-visible:ring-primary-500btn-touchKlasse:min-h-touch min-w-touch(44px)sr-onlyundsr-only-focusableKlassenprefers-reduced-motionMedia Queryaria-hidden="true"auf dekorativen Iconsaria-labelauf Icon-Only-Buttonsaria-busy="true"auf ladenden Buttonsaria-invalidundaria-describedbyauf Inputs mit Fehlern
7. Plugin-UI-Patterns
Neue Plugin-Seite — Checkliste
- 3-Spalten-Layout verwenden (wenn anwendbar): Tree | Liste | Detail
- PluginToolbar registrieren: Create, Import, Export, Search als Toolbar-Items
- Plugin-Settings als eigene Settings-Sub-Seite (Settings-Tree-Navigation)
- Detail-Tabs für Entity-Detail (z.B. "Dateien", "Verlauf", "Notizen")
- EmptyState wenn keine Daten vorhanden
- Skeleton/LoadingState während Daten laden
- Toast nach jeder CRUD-Aktion (Erfolg/Fehler)
- ConfirmDialog vor destruktiven Aktionen
- i18n — alle Texte über
useTranslation()(DE/EN) - Dark Mode — alle Komponenten müssen in Light und Dark funktionieren
Plugin-Toolbar Registrierung
useEffect(() => {
setActivePlugin('myplugin');
setItems([
{ id: 'create', plugin: 'myplugin', type: 'button', label: t('actions.create'), icon: <Plus size={16} />, onClick: handleCreate, group: 'actions' },
{ id: 'import', plugin: 'myplugin', type: 'button', label: t('actions.import'), icon: <Upload size={16} />, onClick: handleImport, group: 'actions' },
{ id: 'search', plugin: 'myplugin', type: 'search', searchPlaceholder: t('search'), onSearch: handleSearch, group: 'search' },
]);
return () => setItems([]);
}, []);
i18n
- Alle Texte über
useTranslation()Hook. - Übersetzungen in
src/i18n/locales/de.jsonundsrc/i18n/locales/en.json. - Keys nach Plugin-Präfix:
myplugin.actions.create,myplugin.search, etc. - Ca. 750 Keys pro Sprache aktuell.
8. Do's & Don'ts
Do's
- ✅ Bestehende UI-Komponenten aus
components/ui/verwenden - ✅
clsxfür bedingte Klassen verwenden - ✅
lucide-reactIcons verwenden (keine inline SVGs) - ✅
date-fnsfür Datumsformatierung verwenden - ✅ Zustand-Stores für State Management verwenden
- ✅ TanStack Query für API-Calls verwenden
- ✅
min-h-touch(44px) für alle interaktiven Elemente - ✅
focus-visible:ring-2für Tastatur-Accessibility - ✅
motion-safe:/motion-reduce:für Animationen - ✅ Toast nach jeder CRUD-Aktion anzeigen
- ✅ ConfirmDialog vor jeder destruktiven Aktion
- ✅ EmptyState für leere Listen
- ✅ Skeleton für Lade-Zustände
Don'ts
- ❌ Keine inline SVGs — immer
lucide-reactverwenden - ❌ Keine
Date.parse()odernew Date()Formatierung —date-fnsverwenden - ❌ Keine hardcoded Farben — Tailwind Design Tokens verwenden
- ❌ Keine
alert()oderconfirm()— Toast und ConfirmDialog verwenden - ❌ Keine CSS-Module oder styled-components — Tailwind-Klassen verwenden
- ❌ Keine
useEffectfür State-Management — Zustand-Stores verwenden - ❌ Keine direkten
fetch()Calls — TanStack Query Hooks verwenden - ❌ Keine
anyTypes — TypeScript-Interfaces definieren - ❌ Keine deutschen Strings im Code — i18n-Keys verwenden
- ❌ Keine
px-Werte für Touch-Targets unter 44px - ❌ Keine
display: nonefür Accessibility-relevante Elemente —sr-onlyverwenden
9. Code-Beispiele
Beispiel: Plugin-Seite mit 3-Spalten-Layout
import { useEffect } from 'react';
import { Plus, Search } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ResizablePanel } from '@/components/ui/ResizablePanel';
import { EmptyState } from '@/components/ui/EmptyState';
import { Button } from '@/components/ui/Button';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
export function MyPluginPage() {
const { t } = useTranslation();
const { setItems, setActivePlugin } = usePluginToolbarStore();
useEffect(() => {
setActivePlugin('myplugin');
setItems([
{ id: 'create', plugin: 'myplugin', type: 'button', label: t('actions.create'), icon: <Plus size={16} />, onClick: handleCreate, group: 'actions' },
{ id: 'search', plugin: 'myplugin', type: 'search', searchPlaceholder: t('search'), onSearch: handleSearch, group: 'search' },
]);
return () => setItems([]);
}, []);
const handleCreate = () => { /* ... */ };
const handleSearch = (q: string) => { /* ... */ };
return (
<div className="flex h-full">
<ResizablePanel initialWidth={224} minWidth={150} maxWidth={600}>
<TreeView />
</ResizablePanel>
<div className="flex-1 overflow-auto">
{items.length === 0 ? (
<EmptyState icon={<FileIcon />} title={t('empty.title')} description={t('empty.description')} action={<Button onClick={handleCreate}>{t('actions.create')}</Button>} />
) : (
<ListView items={items} />
)}
</div>
<div className="flex-1 overflow-auto">
<DetailView />
</div>
</div>
);
}
Beispiel: Formular mit Validation
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { useToast } from '@/components/ui/Toast';
function ContactForm({ open, onClose }) {
const { toast } = useToast();
const [errors, setErrors] = useState({});
const handleSubmit = async (e) => {
e.preventDefault();
try {
await saveContact(formData);
toast({ title: 'Gespeichert', variant: 'success' });
onClose();
} catch (err) {
toast({ title: 'Fehler', description: err.message, variant: 'danger' });
}
};
return (
<Modal open={open} onClose={onClose} title="Kontakt bearbeiten" size="lg">
<form onSubmit={handleSubmit} className="space-y-4">
<Input label="Vorname" error={errors.firstname} required />
<Input label="Nachname" error={errors.surname} required />
<div className="flex justify-end gap-2 pt-4">
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
<Button type="submit" variant="primary">Speichern</Button>
</div>
</form>
</Modal>
);
}
10. Error Boundaries & Plugin-Fehlerbehandlung
ErrorBoundary (common)
Die zentrale ErrorBoundary in components/common/ErrorBoundary.tsx fängt React-Render-Fehler ab und zeigt eine Fallback-UI mit:
- Trace-ID: Jeder Fehler erhält eine eindeutige
trace_id(Format:err-<timestamp>-<random>), die im Fehlerbuffer und im Backend-Log gespeichert wird - Fehlerdetails: Ausklappbare Details mit Fehlermeldung und Stack-Trace
- Retry-Button: Setzt den Error-State zurück und versucht erneut zu rendern
- Neu laden-Button: Lädt die Seite neu (
window.location.reload()) - ARIA:
role="alert"undaria-live="assertive"für Screenreader
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
// Custom Fallback
<ErrorBoundary fallback={(error, retry, traceId) => <CustomErrorUI error={error} retry={retry} traceId={traceId} />}>
<MyComponent />
</ErrorBoundary>
PluginErrorBoundary
Plugin-Seiten werden zusätzlich durch PluginErrorBoundary in PluginLoader.tsx umschlossen. Diese zeigt den Plugin-Namen im Fehlerfall und loggt den Fehler mit pluginName und trace_id.
PluginRouteRenderer
Der PluginRouteRenderer (Catch-All Route) ist mit ErrorBoundary umschlossen, sodass Plugin-Seiten-Fehler nicht die gesamte App crashen.
11. Print & PDF-Export
PrintButton
Die PrintButton-Komponente in components/common/PrintButton.tsx bietet ein Dropdown mit Druck- und PDF-Export-Funktionen:
- Drucken: Ruft
printElement(targetId)oderprintCurrentPage()auf - Als PDF: Ruft
exportToPDF(targetId, filename)auf (Browser-Print-to-PDF) - Print-CSS:
public/print.cssmit@media printRegeln — versteckt Sidebars, TopBar, Buttons und formatiert Tabellen/Card für Druck
import { PrintButton } from '@/components/common/PrintButton';
<PrintButton targetId="contact-detail" filename="kontakt-john-doe" />
Verwendung in: ContactDetailPage, Reports, Calendar.
12. API-Dokumentation (Swagger UI)
Die ApiDocsPage in pages/ApiDocs.tsx bettet die FastAPI Swagger UI in einem iframe ein:
- Route:
/api-docs(erfordertsettings:readPermission) - iframe src:
/docs(FastAPI Swagger UI) - External-Link: Öffnet
/docsin neuem Tab - Vollbild-iframe mit Header-Leiste
13. Notification-System
NotificationBell + Dropdown
Die NotificationBell in components/layout/NotificationBell.tsx zeigt ungelesene Benachrichtigungen mit:
- Unread-Badge: Roter Zähler (max. 99+) mit 30s Polling
- Dropdown: Liste der Benachrichtigungen, „Alle als gelesen"-Button
- System-Channel-Link: Öffnet den Kommunikations-System-Channel unter
/communication?channel=system - Alle anzeigen: Navigiert zu
/settings/notifications
Das Backend delegiert /notifications an den Kommunikations-System-Channel (post_system_message).
14. Accessibility (A11Y) Patterns
Verbindliche ARIA-Regeln
| Element | Attribut | Wert |
|---|---|---|
| Buttons | aria-label |
Beschreibung der Aktion |
| Icon-only Buttons | aria-label + aria-hidden auf Icon |
Icon dekorativ |
| Dropdowns | aria-haspopup="menu" + aria-expanded |
true/false |
| Menüs | role="menu" + aria-label |
Menüname |
| Error-Bereiche | role="alert" + aria-live="assertive" |
Für Screenreader |
| Navigation | aria-label auf <nav> und <aside> |
„Hauptnavigation" etc. |
| Skip-Link | sr-only focus:not-sr-only |
„Zum Hauptinhalt springen" |
Touch Targets
Alle interaktiven Elemente müssen min-h-touch (44px) und min-w-touch (44px) haben:
<button className="min-h-touch min-w-touch ...">Klick</button>
Focus-Management
- Sichtbarer Focus-Ring:
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 - Skip-Link in
App.tsx:sr-only focus:not-sr-only main-Element mittabIndex={-1}für Tastatur-Fokus
15. Datei-Struktur
frontend/src/
├── components/
│ ├── ui/ # Basis-Komponenten (Button, Card, Modal, etc.)
│ ├── layout/ # Layout-Komponenten (PluginToolbar, etc.)
│ └── [plugin]/ # Plugin-spezifische Komponenten
├── pages/ # Seiten-Komponenten (Routes)
├── store/ # Zustand-Stores
├── hooks/ # Custom Hooks (aufgeteilt nach Domain)
├── utils/ # Utilities (date.ts, api.ts, etc.)
├── i18n/ # Übersetzungen
│ └── locales/
│ ├── de.json
│ └── en.json
└── routes/ # React Router Konfiguration
Diese Richtlinien sind verbindlich für alle Frontend-Entwicklung an LeoCRM.