managarten/.claude/guidelines/design-ux.md
Till JS fc028fa8f0 chore(lint): audit:theme-tokens guard against bare --muted / --theme-* drift
Three naming conventions had drifted through the monorepo (--muted, --theme-*,
--color-*). Only the last is defined in the Mana theme; the others silently
fell back to nothing and stopped tracking theme variants. Today's cleanup
migrated ~100 files, but nothing stopped the drift from creeping back.

- scripts/audit-theme-tokens.mjs scans ~3k source files and fails if any
  references a bare shadcn token or a --theme-* prefix, with an allowlist
  for known-literal module brand colors (news-research, agent templates)
- wire into pnpm script and lint-staged (runs once per commit touching
  *.{svelte,css}, ignores per-file args)
- design-ux.md guideline: fix stale --color-destructive entry (Mana uses
  --color-error), add explicit "never bare tokens" warning with examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:58:13 +02:00

547 lines
15 KiB
Markdown

# Design & UX Guidelines
Richtlinien für konsistentes Design und User Experience im ManaCore Monorepo.
## Grundprinzipien
### 1. Inline vor Modal
**Bevorzuge Inline-Interaktionen** statt separater Modals, um Kontext zu bewahren und visuelle Unruhe zu vermeiden.
```
BEVORZUGT: Inline-Expansion
┌─────────────────────────────┐
│ ○ Task Title ▼ │ ← Klick klappt auf
├─────────────────────────────┤
│ [Titel-Input] │
│ [Beschreibung] │
│ [Weitere Felder...] │
│ [Abbrechen] [Speichern] │
└─────────────────────────────┘
VERMEIDEN: Separate Modals für einfache Bearbeitungen
```
**Ausnahmen für Modals:**
- Bestätigungsdialoge (Löschen, kritische Aktionen)
- Komplexe Wizards mit mehreren Schritten
- Vollbild-Medienansichten
- Auth-Gates (Login-Aufforderung)
### 2. Mobile-First
Designs beginnen mit der mobilen Ansicht und werden für größere Screens erweitert.
```css
/* Mobile-First Breakpoints */
@media (min-width: 640px) {
/* sm */
}
@media (min-width: 768px) {
/* md */
}
@media (min-width: 1024px) {
/* lg */
}
@media (min-width: 1280px) {
/* xl */
}
```
### 3. Reduktion vor Addition
- Weniger UI-Elemente = bessere UX
- Leere Sektionen ausblenden statt "Keine Daten" anzeigen
- Progressive Disclosure: Details erst bei Bedarf zeigen
## Layout & Spacing
### Spacing-System
Verwende konsistente Abstände basierend auf 4px-Raster:
| Token | Wert | Verwendung |
| --------- | ---- | --------------------------------- |
| `0.25rem` | 4px | Minimaler Abstand (Icons, Badges) |
| `0.5rem` | 8px | Kompakte Elemente |
| `0.75rem` | 12px | Standard innerhalb Komponenten |
| `1rem` | 16px | Standard zwischen Elementen |
| `1.5rem` | 24px | Sektions-Padding |
| `2rem` | 32px | Große Abstände |
### Container & Max-Width
```css
/* Standard Content Container */
max-width: 640px; /* Formulare, Listen */
max-width: 800px; /* Breitere Inhalte */
max-width: 1200px; /* Dashboard-Layouts */
```
### Border-Radius
| Verwendung | Wert |
| ------------------------------ | ------------------------------ |
| Kleine Elemente (Badges, Tags) | `9999px` (pill) oder `0.25rem` |
| Buttons, Inputs | `0.5rem` - `0.75rem` |
| Cards, Modals | `0.75rem` - `1.5rem` |
| Große Container | `1rem` - `1.5rem` |
## Farben & Theming
### CSS Custom Properties
Alle Farben über CSS-Variablen definieren für Dark Mode Kompatibilität:
```css
/* Richtig */
color: hsl(var(--color-foreground));
background: hsl(var(--color-surface));
border-color: hsl(var(--color-border));
/* Falsch - hardcoded Farben */
color: #374151;
background: white;
```
### Semantische Farben
| Variable | Verwendung |
| -------------------------- | ------------------------------ |
| `--color-primary` | Primäre Aktionen, Links, Fokus |
| `--color-foreground` | Haupttext |
| `--color-muted-foreground` | Sekundärtext, Platzhalter |
| `--color-surface` | Hintergründe |
| `--color-border` | Rahmen, Trennlinien |
| `--color-error` | Lösch-Aktionen, Fehler |
| `--color-success` | Erfolgsmeldungen |
| `--color-warning` | Warnungen |
### ⚠️ Nie bare shadcn-Tokens
Manche Komponenten aus shadcn- oder theme-Vorlagen sprechen bare Token-Namen
an (`--muted`, `--primary`, `--theme-muted`). **Diese existieren im Mana-Theme
nicht** und fallen stumm aus — das Theme-Wechseln wird nicht mehr mitgezogen.
Immer den `--color-*`-Präfix nutzen:
```css
/* Falsch */
background: hsl(var(--muted));
color: var(--foreground, #111);
border: 1px solid var(--theme-border);
/* Richtig */
background: hsl(var(--color-muted));
color: hsl(var(--color-foreground));
border: 1px solid hsl(var(--color-border));
```
Der Audit `pnpm audit:theme-tokens` (läuft auch in `lint-staged`) schlägt
jeden Drift an.
### Dark Mode
Immer beide Modi berücksichtigen:
```css
.element {
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.08);
}
:global(.dark) .element {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
}
```
## Animationen & Transitions
### Standard-Timings
| Typ | Dauer | Easing |
| ------------------------- | --------------- | ------------- |
| Micro-Interaktionen | `150ms` | `ease` |
| UI-Feedback | `200ms` | `ease-out` |
| Layout-Änderungen | `200ms - 300ms` | `ease-out` |
| Aufmerksamkeits-Animation | `2s - 3s` | `ease-in-out` |
### Transition-Beispiele
```css
/* Hover-Effekte */
transition: all 0.15s ease;
/* Expand/Collapse */
transition: all 0.2s ease-out;
/* Subtile Aufmerksamkeit (z.B. Float-Animation) */
animation: float 3s ease-in-out infinite;
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
```
### Wann animieren
**Ja:**
- Zustandsänderungen (expand, collapse, toggle)
- Feedback bei Aktionen (Checkbox, Button-Press)
- Aufmerksamkeit lenken (leere Zustände)
- Smooth Scrolling
**Nein:**
- Initiales Laden von Inhalten
- Jeder einzelne Listen-Eintrag
- Performance-kritische Bereiche
- Wenn `prefers-reduced-motion` aktiv
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
## Komponenten-Patterns
### Buttons
```
Hierarchie:
1. Primary → Hauptaktion (1x pro Ansicht)
2. Secondary → Alternative Aktionen
3. Ghost → Tertiäre Aktionen, Links
4. Danger → Destruktive Aktionen
```
```css
/* Primary */
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
/* Secondary */
background: rgba(0, 0, 0, 0.05);
color: hsl(var(--color-foreground));
/* Danger */
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
```
### Inputs & Forms
- Labels immer über dem Input
- Placeholder für Hinweise, nicht als Label-Ersatz
- Fokus-Ring mit Primary-Farbe
- Fehler-States mit rotem Border + Fehlermeldung
```css
.input:focus {
outline: none;
border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 2px hsl(var(--color-primary) / 0.1);
}
```
### Cards & Listen-Elemente
Glassmorphism-Stil für erhöhte Elemente:
```css
.card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 0.75rem;
}
```
### Empty States
Leere Zustände sollten:
1. Visuell ansprechend sein (Icon, Illustration)
2. Erklären was hier normalerweise ist
3. Eine klare Handlungsaufforderung bieten
```
┌─────────────────────────────┐
│ │
│ [Animiertes Icon] │
│ │
│ Motivierender Titel │
│ Kurze Beschreibung │
│ │
│ [Primäre Aktion] │
│ │
└─────────────────────────────┘
```
### Loading States
- Skeleton-Loader für bekannte Layouts
- Spinner für unbekannte Ladezeiten
- Inline-Spinner in Buttons während Aktionen
```svelte
{#if isLoading}
<TaskListSkeleton sections={3} tasksPerSection={3} />
{:else}
<TaskList {tasks} />
{/if}
```
## Feedback & Kommunikation
### Erfolgsmeldungen
- Subtil, nicht störend
- Automatisch ausblenden nach 3-5 Sekunden
- Toast-Benachrichtigungen für asynchrone Aktionen
### Fehlermeldungen
- Inline bei Formularen (direkt beim Feld)
- Toast/Banner für globale Fehler
- Klare Sprache: Was ist passiert + Was kann man tun
### Bestätigungsdialoge
Nur für:
- Unwiderrufliche Aktionen (Löschen)
- Aktionen mit weitreichenden Konsequenzen
- Wenn Datenverlust möglich ist
```
Titel: "Aufgabe löschen?"
Text: "Diese Aufgabe wird unwiderruflich gelöscht."
[Abbrechen] [Löschen]
```
## Accessibility (a11y)
### Mindestanforderungen
1. **Tastatur-Navigation**: Alle interaktiven Elemente erreichbar
2. **Fokus-Indikatoren**: Sichtbarer Fokus-Ring
3. **Kontrast**: Mindestens 4.5:1 für Text
4. **Screen Reader**: Semantisches HTML, ARIA-Labels
### ARIA-Patterns
```svelte
<!-- Dialog -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Titel</h2>
</div>
<!-- Button mit Icon -->
<button aria-label="Bearbeiten">
<EditIcon />
</button>
<!-- Expandable -->
<button aria-expanded={isExpanded}> Details </button>
```
### Keyboard Shortcuts
| Aktion | Shortcut |
| ------------------- | ------------------- |
| Speichern | `Cmd/Ctrl + Enter` |
| Abbrechen/Schließen | `Escape` |
| Navigation | `Tab` / `Shift+Tab` |
### Listen-Navigation mit Tastatur
In Listen (Tasks, Kontakte, Favoriten etc.) soll der Nutzer sich nahtlos per Tastatur bewegen können, ohne dass Fokus auf nicht-editierbare Elemente (Checkboxes, Drag-Handles) springt.
**Prinzip: Zirkuläre Navigation zwischen Input und Liste**
```
InputBar → ArrowUp/Tab → Erster Listeneintrag
↕ ArrowUp/Down/Tab/Shift+Tab
Letzter Eintrag → ArrowDown/Tab → InputBar
```
**Implementierung mit `contenteditable`:**
```svelte
<!-- Immer editierbar, kein Mode-Switch -->
<span
contenteditable="true"
role="textbox"
spellcheck="true"
onkeydown={handleKeydown}
onblur={handleBlur}
>
{item.title}
</span>
```
**Keydown-Handler für Listen-Navigation:**
```typescript
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
currentRef?.blur(); // Speichert via onblur
} else if (e.key === 'Escape') {
currentRef.textContent = originalValue; // Verwerfen
currentRef?.blur();
} else if (e.key === 'Tab' || e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const direction = e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey) ? -1 : 1;
e.preventDefault();
// Alle editierbaren Elemente auf der Seite sammeln
const allEditable = Array.from(
document.querySelectorAll<HTMLElement>('.item-title[contenteditable]')
);
const currentIndex = allEditable.indexOf(currentRef!);
const next = allEditable[currentIndex + direction];
currentRef?.blur();
if (next) {
next.focus();
} else {
// Am Rand der Liste: Fokus zur InputBar
document.querySelector<HTMLInputElement>('.quick-input-bar input')?.focus();
}
}
}
```
**Von der InputBar in die Liste (Page-Level Handler):**
```svelte
<svelte:window onkeydown={(e) => {
const target = e.target as HTMLElement;
if (target.closest('.quick-input-bar') && (e.key === 'ArrowUp' || e.key === 'Tab')) {
const first = document.querySelector<HTMLElement>('.item-title[contenteditable]');
if (first) {
e.preventDefault();
first.focus();
}
}
}} />
```
**Regeln:**
- `contenteditable` statt Input-Toggle: Cursor landet direkt an Klick-Position
- Kein visueller Unterschied zwischen Lese- und Edit-Modus (kein Border, kein Hintergrund)
- Speichern via `blur`-Event (Auto-Save), nicht via explizitem Save-Button
- ArrowDown/Up navigiert vertikal, Tab/Shift+Tab ebenfalls (gleiche Richtung)
- Am Listenende/-anfang springt Fokus zurück zur InputBar (zirkulär)
- Hover-Effekte auf Listeneinträgen vermeiden (ruhige UI)
### Inline-Editing mit contenteditable
**Bevorzuge `contenteditable` statt Input-Toggle** für Texte, die direkt in der Ansicht editiert werden.
| Aspekt | Input-Toggle | contenteditable |
| -------------------- | ------------------------------ | ---------------------------- |
| Klicks zum Editieren | 2 (aktivieren + positionieren) | 1 (Cursor an Klick-Position) |
| DOM-Wechsel | `<span>``<input>` | Keiner |
| Mehrzeilig | Braucht `<textarea>` | Nativ |
| Styling | Muss Input an Span anpassen | Gleich |
**Wann Input statt contenteditable:**
- Formular-Felder mit Validierung (Datum, Zahl, E-Mail)
- Wenn `type="date"`, `type="number"` etc. benötigt wird
- In Expanded-Forms/Modals (nicht inline)
### Debug-Borders (Entwickler-Tool)
`Ctrl+Shift+D` aktiviert farbkodierte Outlines für alle UI-Elemente. Nutzt `outline` statt `border` um Layout nicht zu beeinflussen. State wird in localStorage persistiert.
```css
.debug-mode * {
outline: 1px solid rgba(255, 0, 0, 0.3) !important;
}
.debug-mode div {
outline-color: rgba(255, 0, 0, 0.4) !important;
}
.debug-mode section {
outline-color: rgba(0, 100, 255, 0.5) !important;
}
.debug-mode nav {
outline-color: rgba(0, 180, 0, 0.5) !important;
}
.debug-mode button {
outline-color: rgba(255, 220, 0, 0.6) !important;
}
.debug-mode input,
.debug-mode textarea {
outline-color: rgba(139, 92, 246, 0.6) !important;
}
```
Aktuell in Todo implementiert, geplant als `@manacore/shared-debug` Package.
## Z-Index-Hierarchie
Konsistente Schichtung für Overlays:
| Element | Z-Index |
| ---------------- | ------------ |
| Normale Inhalte | `auto` / `0` |
| Sticky Headers | `10` |
| Dropdowns | `50` |
| Fixed Navigation | `90-100` |
| Modals/Dialoge | `9995` |
| Toasts | `9999` |
**Wichtig:** Keine `z-index: 0` auf Containern setzen - das erstellt einen neuen Stacking Context und "trapped" Kind-Elemente.
## Typografie
### Font-Sizes
```css
text-xs: 0.75rem /* 12px - Labels, Badges */
text-sm: 0.875rem /* 14px - Sekundärtext */
text-base: 1rem /* 16px - Standardtext */
text-lg: 1.125rem /* 18px - Subtitles */
text-xl: 1.25rem /* 20px - Titles */
text-2xl: 1.5rem /* 24px - Page Headers */
```
### Font-Weights
```css
font-normal: 400 /* Body Text */
font-medium: 500 /* UI-Elemente, Buttons */
font-semibold: 600 /* Überschriften, Labels */
font-bold: 700 /* Wichtige Titel */
```
## Checkliste für neue Features
- [ ] Mobile-First implementiert?
- [ ] Dark Mode getestet?
- [ ] Inline-Interaktion statt Modal möglich?
- [ ] Loading/Error/Empty States vorhanden?
- [ ] Tastatur-Navigation funktioniert?
- [ ] Listen: Arrow/Tab-Navigation zwischen Einträgen?
- [ ] Listen: Zirkuläre Navigation zu InputBar?
- [ ] Inline-Editing: contenteditable statt Input-Toggle?
- [ ] Keine Hover-Effekte auf Listeneinträgen?
- [ ] Animations sparsam und sinnvoll?
- [ ] CSS-Variablen statt hardcoded Farben?
- [ ] Konsistente Spacing-Werte?