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:
@@ -0,0 +1,348 @@
|
||||
# LeoCRM Plugin Development Guide
|
||||
|
||||
> **Version:** 1.0
|
||||
> **Datum:** 2026-07-23
|
||||
> **Gültig für:** Alle Plugin-Entwickler
|
||||
|
||||
---
|
||||
|
||||
## 1. Plugin-Struktur
|
||||
|
||||
Jedes Plugin liegt unter `app/plugins/builtins/<plugin_name>/`:
|
||||
|
||||
```
|
||||
app/plugins/builtins/my_plugin/
|
||||
├── __init__.py
|
||||
├── plugin.py # Plugin-Klasse mit Manifest
|
||||
├── routes.py # API-Routes
|
||||
├── models.py # SQLAlchemy-Modelle (optional)
|
||||
├── schemas.py # Pydantic-Schemas (optional)
|
||||
├── services.py # Business-Logik (optional)
|
||||
├── migrations/ # SQL-Migrationen
|
||||
│ └── 0001_initial.sql
|
||||
└── tests/ # Plugin-Tests (optional)
|
||||
```
|
||||
|
||||
## 2. Plugin-Manifest
|
||||
|
||||
Das Manifest definiert Metadaten, Abhängigkeiten, Routes, Events und Permissions:
|
||||
|
||||
```python
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import PluginManifest, PluginRouteDef
|
||||
|
||||
class MyPlugin(BasePlugin):
|
||||
manifest = PluginManifest(
|
||||
name="my_plugin",
|
||||
version="1.0.0",
|
||||
display_name="My Plugin",
|
||||
description="Description of what the plugin does.",
|
||||
dependencies=["permissions"], # Other plugins this depends on
|
||||
routes=[
|
||||
PluginRouteDef(
|
||||
path="/api/v1/my-plugin",
|
||||
module="app.plugins.builtins.my_plugin.routes",
|
||||
router_attr="router",
|
||||
),
|
||||
],
|
||||
events=["contact.created", "contact.updated"],
|
||||
migrations=["0001_initial.sql"],
|
||||
permissions=[
|
||||
"my_plugin:read",
|
||||
"my_plugin:write",
|
||||
"my_plugin:delete",
|
||||
],
|
||||
agent_capabilities=[
|
||||
"my_plugin:search",
|
||||
"my_plugin:analyze",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Manifest-Felder
|
||||
|
||||
| Feld | Typ | Pflicht | Beschreibung |
|
||||
|---|---|---|---|
|
||||
| `name` | `str` | Ja | Eindeutiger Plugin-Name (snake_case) |
|
||||
| `version` | `str` | Ja | Semantic Version |
|
||||
| `display_name` | `str` | Ja | Anzeigename |
|
||||
| `description` | `str` | Nein | Kurzbeschreibung |
|
||||
| `dependencies` | `list[str]` | Nein | Andere Plugins, die geladen sein müssen |
|
||||
| `routes` | `list[PluginRouteDef]` | Nein | API-Routen-Definitionen |
|
||||
| `events` | `list[str]` | Nein | Events, die das Plugin abonniert |
|
||||
| `migrations` | `list[str]` | Nein | SQL-Migrationsdateien |
|
||||
| `permissions` | `list[str]` | Nein | RBAC-Permissions, die das Plugin definiert |
|
||||
| `field_definitions` | `list[FieldDefinition]` | Nein | Feld-Level-Permissions |
|
||||
| `agent_capabilities` | `list[str]` | Nein | KI-Agent-Fähigkeiten, die das Plugin bietet |
|
||||
| `is_core` | `bool` | Nein | Core-Plugin (kann nicht deaktiviert werden) |
|
||||
|
||||
## 3. RBAC-Permissions
|
||||
|
||||
### Permissions definieren
|
||||
|
||||
Im Manifest werden alle Permissions des Plugins aufgelistet:
|
||||
|
||||
```python
|
||||
permissions=[
|
||||
"my_plugin:read",
|
||||
"my_plugin:write",
|
||||
"my_plugin:delete",
|
||||
"my_plugin:admin",
|
||||
],
|
||||
```
|
||||
|
||||
### Routes absichern
|
||||
|
||||
Jede Route muss mit `require_permission` abgesichert werden:
|
||||
|
||||
```python
|
||||
from app.deps import get_current_user, require_permission
|
||||
from fastapi import Depends
|
||||
|
||||
@router.get("", dependencies=[Depends(require_permission("my_plugin:read"))])
|
||||
async def list_items(current_user: dict = Depends(get_current_user)):
|
||||
...
|
||||
|
||||
@router.post("", status_code=201, dependencies=[Depends(require_permission("my_plugin:write"))])
|
||||
async def create_item(data: ItemCreate, current_user: dict = Depends(get_current_user)):
|
||||
...
|
||||
|
||||
@router.delete("/{item_id}", dependencies=[Depends(require_permission("my_plugin:delete"))])
|
||||
async def delete_item(item_id: str, current_user: dict = Depends(get_current_user)):
|
||||
...
|
||||
```
|
||||
|
||||
### Permission-Namenskonvention
|
||||
|
||||
- Format: `<plugin_name>:<action>`
|
||||
- Standard-Actions: `read`, `write`, `delete`, `share`, `admin`
|
||||
- Beispiele: `calendar:read`, `dms:write`, `tags:delete`
|
||||
|
||||
## 4. KI-Agent-Framework
|
||||
|
||||
LeoCRM bietet ein integriertes KI-Agent-Framework basierend auf **LiteLLM** und **PydanticAI**.
|
||||
|
||||
### Architektur
|
||||
|
||||
```
|
||||
Plugin (ai_assistant, ai_proactive, zukünftige)
|
||||
↓
|
||||
LiteLLM (unified LLM interface — 100+ Provider)
|
||||
↓
|
||||
Provider (OpenAI, Anthropic, Google, Ollama, ...)
|
||||
↑
|
||||
Tool Registry (Plugin-Tools für KI-Agenten)
|
||||
```
|
||||
|
||||
### LiteLLM — Unified LLM Interface
|
||||
|
||||
LiteLLM bietet eine einheitliche API für über 100 LLM-Provider. Alle KI-Funktionen
|
||||
in LeoCRM nutzen `litellm.acompletion()`:
|
||||
|
||||
```python
|
||||
import litellm
|
||||
|
||||
response = await litellm.acompletion(
|
||||
model="openai/gpt-4o", # oder anthropic/claude-3-sonnet, ollama/llama3
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_query},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=1000,
|
||||
api_key=os.environ.get("AI_API_KEY"),
|
||||
api_base=os.environ.get("AI_API_BASE"), # optional für self-hosted
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
```
|
||||
|
||||
### Konfiguration
|
||||
|
||||
| Env-Var | Beschreibung | Standard |
|
||||
|---|---|---|
|
||||
| `AI_MODEL` | Modell-Name (z.B. `gpt-4o`, `claude-3-sonnet`, `llama3`) | — |
|
||||
| `AI_API_KEY` | API-Key für den Provider | — |
|
||||
| `AI_API_BASE` | Custom API-Base-URL (optional) | Provider-Standard |
|
||||
| `AI_PROVIDER` | Provider-Präfix (`openai`, `anthropic`, `google`, `ollama`) | `openai` |
|
||||
|
||||
Wenn `AI_MODEL` und `AI_API_KEY` nicht gesetzt sind, läuft der LLM-Client im Mock-Modus
|
||||
(keyword-basierte Action-Mapping für Tests).
|
||||
|
||||
### Tool Registry — KI-Tools registrieren
|
||||
|
||||
Plugins können Tools registrieren, die KI-Agenten während Chat-Sessions aufrufen können.
|
||||
Jedes Tool deklariert Name, Beschreibung, JSON-Schema für Parameter und einen async Handler.
|
||||
|
||||
```python
|
||||
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
|
||||
|
||||
registry = get_tool_registry()
|
||||
|
||||
registry.register(
|
||||
name="search_contacts",
|
||||
description="Search contacts by name, email, or phone number",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
"limit": {"type": "integer", "description": "Max results", "default": 10},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
handler=my_search_handler,
|
||||
plugin_name="my_plugin",
|
||||
required_permission="contacts:read",
|
||||
category="search",
|
||||
)
|
||||
```
|
||||
|
||||
### Tool Handler
|
||||
|
||||
Der Handler ist eine async Funktion, die Argumente und Kontext empfängt:
|
||||
|
||||
```python
|
||||
async def my_search_handler(arguments: dict, context: dict) -> str:
|
||||
query = arguments.get("query", "")
|
||||
limit = arguments.get("limit", 10)
|
||||
# ... perform search ...
|
||||
return json.dumps({"results": results})
|
||||
```
|
||||
|
||||
### Tools bei Plugin-Deaktivierung abmelden
|
||||
|
||||
```python
|
||||
def on_deactivate(self):
|
||||
registry = get_tool_registry()
|
||||
registry.unregister_plugin("my_plugin")
|
||||
```
|
||||
|
||||
### Agent Capabilities im Manifest
|
||||
|
||||
Das `agent_capabilities` Feld im Manifest deklariert, welche KI-Fähigkeiten ein Plugin bietet:
|
||||
|
||||
```python
|
||||
agent_capabilities=[
|
||||
"contact_search", # Kontakt-Suche
|
||||
"email_draft", # E-Mail-Entwürfe generieren
|
||||
"calendar_scheduling", # Terminvorschläge
|
||||
],
|
||||
```
|
||||
|
||||
Diese Informationen werden vom AI Assistant verwendet, um Nutzern zu zeigen,
|
||||
welche KI-Funktionen verfügbar sind.
|
||||
|
||||
## 5. Events
|
||||
|
||||
Plugins können Events abonnieren und auslösen:
|
||||
|
||||
```python
|
||||
# Im Manifest:
|
||||
events=["contact.created", "contact.updated", "contact.deleted"]
|
||||
|
||||
# Event-Handler im Plugin:
|
||||
async def on_contact_created(self, event_data: dict):
|
||||
# Reagiere auf neues Kontakt-Event
|
||||
pass
|
||||
```
|
||||
|
||||
Events werden vom Event-Publisher im Contact-Service ausgelöst:
|
||||
|
||||
```python
|
||||
from app.core.events import publish_event
|
||||
await publish_event(db, "contact.created", {"contact_id": str(contact.id)})
|
||||
```
|
||||
|
||||
## 6. Datenbank-Migrationen
|
||||
|
||||
SQL-Migrationen liegen unter `migrations/` im Plugin-Verzeichnis:
|
||||
|
||||
```sql
|
||||
-- migrations/0001_initial.sql
|
||||
CREATE TABLE my_plugin_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
Im Manifest referenzieren:
|
||||
```python
|
||||
migrations=["0001_initial.sql"]
|
||||
```
|
||||
|
||||
## 7. UI-Integration
|
||||
|
||||
Siehe `docs/ui-design-guidelines.md` für Frontend-Konventionen.
|
||||
|
||||
- Plugin-Seiten verwenden das 3-Spalten-Explorer-Layout
|
||||
- PluginToolbar für Aktionen
|
||||
- Plugin-Settings als eigene Settings-Sub-Seite
|
||||
- i18n-Keys mit Plugin-Präfix
|
||||
|
||||
## 8. Testing
|
||||
|
||||
Tests liegen unter `tests/` im Plugin-Verzeichnis oder im zentralen `tests/` Ordner:
|
||||
|
||||
```python
|
||||
# tests/test_my_plugin.py
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_items_requires_permission(client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/my-plugin", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_items_without_permission_returns_403(client: AsyncClient, no_perm_headers):
|
||||
response = await client.get("/api/v1/my-plugin", headers=no_perm_headers)
|
||||
assert response.status_code == 403
|
||||
```
|
||||
|
||||
## 9. Plugin-Beispiel
|
||||
|
||||
Minimal-Beispiel für ein neues Plugin:
|
||||
|
||||
```python
|
||||
# app/plugins/builtins/my_plugin/plugin.py
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import PluginManifest, PluginRouteDef
|
||||
|
||||
class MyPlugin(BasePlugin):
|
||||
manifest = PluginManifest(
|
||||
name="my_plugin",
|
||||
version="1.0.0",
|
||||
display_name="My Plugin",
|
||||
description="A minimal example plugin.",
|
||||
dependencies=[],
|
||||
routes=[
|
||||
PluginRouteDef(
|
||||
path="/api/v1/my-plugin",
|
||||
module="app.plugins.builtins.my_plugin.routes",
|
||||
router_attr="router",
|
||||
),
|
||||
],
|
||||
events=[],
|
||||
migrations=[],
|
||||
permissions=["my_plugin:read", "my_plugin:write"],
|
||||
agent_capabilities=[],
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
# app/plugins/builtins/my_plugin/routes.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.deps import get_current_user, require_permission
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("", dependencies=[Depends(require_permission("my_plugin:read"))])
|
||||
async def list_items(current_user: dict = Depends(get_current_user)):
|
||||
return {"items": []}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Dieses Dokument ist verbindlich für alle Plugin-Entwicklung an LeoCRM.*
|
||||
@@ -0,0 +1,535 @@
|
||||
# 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.*
|
||||
Reference in New Issue
Block a user