# 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-50` für Card-Footer, `secondary-100` fü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 `ConfirmDialog` kombinieren.
- **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-900` als Hintergrund und `secondary-50` als 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-Header
- `font-semibold` (600) — Card-Titel, Seiten-Titel
- `font-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: `ResizablePanel` mit `initialWidth=224`, `minWidth=150`, `maxWidth=600`
- Mittlere Spalte: `ResizablePanel` mit `resizable=false` (flex-1)
- Rechte Spalte: `ResizablePanel` mit `resizable=false` oder `handleSide="left"`
- Drag-Handle auf der rechten Kante der linken Spalte

```tsx
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`:

```tsx
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 `group` gruppiert mit Trennern zwischen Gruppen.
- Button-Labels sind auf Mobile (`hidden sm:inline`) ausgeblendet, Icons bleiben sichtbar.
- Toolbar-Höhe: `min-h-[43px]`, Hintergrund `bg-white`, Border-Bottom `border-secondary-200`.

### Modal-Dialoge

Für Formulare, Bestätigungen und Dialoge:

```tsx
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 |

- `ConfirmDialog` fü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

```tsx
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-2` für Tastatur-Navigation.
- `motion-safe:duration-200` für Übergänge (respektiert `prefers-reduced-motion`).

### Card

```tsx
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

```tsx
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 |

- `dot` prop zeigt einen farbigen Punkt links an.
- Größe: `text-xs`, `px-2.5 py-0.5`, `rounded-full`.

### Input / Select

```tsx
import { Input } from '@/components/ui/Input';

<Input label="Name" error={errors.name} helperText="Vollständiger Name" required />
```

- `focus:ring-2 focus:ring-primary-500` bei Fokus.
- Error-State: `border-danger-500`, `text-danger-900`.
- `aria-invalid`, `aria-describedby` fü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-label` auf sortierbare Headers.
- Zebra-Stiping optional: `even:bg-secondary-50`.

### Toast

```tsx
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

```tsx
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

```tsx
import { Skeleton } from '@/components/ui/Skeleton';

<Skeleton className="h-8 w-full" />
```

- Verwendet während Daten laden.
- `animate-pulse` Animation.
- 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

1. **Focus-Ring**: Alle interaktiven Elemente haben `focus-visible:ring-2 focus-visible:ring-primary-500`.
2. **Touch-Targets**: Mindestens 44×44px (`min-h-touch min-w-touch`).
3. **ARIA-Labels**: Dekorative SVGs erhalten `aria-hidden="true"`. Interaktive Elemente ohne sichtbaren Text erhalten `aria-label`.
4. **Screen Reader**: `sr-only` Klasse für Text nur für Screen Reader. `sr-only-focusable` für Skip-Links.
5. **Reduced Motion**: `motion-safe:` und `motion-reduce:` Präfixe verwenden. `prefers-reduced-motion` Media Query wird respektiert.
6. **Tastatur-Navigation**: Tab-Reihenfolge folgt visueller Reihenfolge. Escape schließt Modals/Dropdowns.
7. **Farbkontrast**: Mindestens 4.5:1 für Body-Text, 3:1 für große Texte und UI-Komponenten.

### Implementierte Patterns

- `focus-ring` Klasse: `focus-visible:ring-2 focus-visible:ring-primary-500`
- `btn-touch` Klasse: `min-h-touch min-w-touch` (44px)
- `sr-only` und `sr-only-focusable` Klassen
- `prefers-reduced-motion` Media Query
- `aria-hidden="true"` auf dekorativen Icons
- `aria-label` auf Icon-Only-Buttons
- `aria-busy="true"` auf ladenden Buttons
- `aria-invalid` und `aria-describedby` auf Inputs mit Fehlern

---

## 7. Plugin-UI-Patterns

### Neue Plugin-Seite — Checkliste

1. **3-Spalten-Layout** verwenden (wenn anwendbar): Tree | Liste | Detail
2. **PluginToolbar** registrieren: Create, Import, Export, Search als Toolbar-Items
3. **Plugin-Settings** als eigene Settings-Sub-Seite (Settings-Tree-Navigation)
4. **Detail-Tabs** für Entity-Detail (z.B. "Dateien", "Verlauf", "Notizen")
5. **EmptyState** wenn keine Daten vorhanden
6. **Skeleton/LoadingState** während Daten laden
7. **Toast** nach jeder CRUD-Aktion (Erfolg/Fehler)
8. **ConfirmDialog** vor destruktiven Aktionen
9. **i18n** — alle Texte über `useTranslation()` (DE/EN)
10. **Dark Mode** — alle Komponenten müssen in Light und Dark funktionieren

### Plugin-Toolbar Registrierung

```tsx
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.json` und `src/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
- ✅ `clsx` für bedingte Klassen verwenden
- ✅ `lucide-react` Icons verwenden (keine inline SVGs)
- ✅ `date-fns` fü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-2` fü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-react` verwenden
- ❌ Keine `Date.parse()` oder `new Date()` Formatierung — `date-fns` verwenden
- ❌ Keine hardcoded Farben — Tailwind Design Tokens verwenden
- ❌ Keine `alert()` oder `confirm()` — Toast und ConfirmDialog verwenden
- ❌ Keine CSS-Module oder styled-components — Tailwind-Klassen verwenden
- ❌ Keine `useEffect` für State-Management — Zustand-Stores verwenden
- ❌ Keine direkten `fetch()` Calls — TanStack Query Hooks verwenden
- ❌ Keine `any` Types — TypeScript-Interfaces definieren
- ❌ Keine deutschen Strings im Code — i18n-Keys verwenden
- ❌ Keine `px-` Werte für Touch-Targets unter 44px
- ❌ Keine `display: none` für Accessibility-relevante Elemente — `sr-only` verwenden

---

## 9. Code-Beispiele

### Beispiel: Plugin-Seite mit 3-Spalten-Layout

```tsx
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

```tsx
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. 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.*
