Files
leocrm/docs/ui-design-guidelines.md
T
Agent Zero ec81940178 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)
2026-07-23 08:42:26 +02:00

18 KiB
Raw Blame History

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

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

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

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
  • dot prop 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-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

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

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

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