# UI Components Specification

**Version:** 1.0  
**Datum:** 2026-04-24  
**Status:** DRAFT - Awaiting Review  
**Autor:** Claude Code

---

## 1. Base Components (Shared)

### 1.1 Chip

**Beschreibung:** Kleiner Badge/Tag für Kategorien, Skills, Zuordnungen

**Props:**
```typescript
{
  label: string              // required
  primary?: boolean          // default: false
  color?: string             // optional, CSS var name (z.B. 'var(--accent)')
  size?: 'sm' | 'md' | 'lg'  // default: 'md'
  onDelete?: () => void      // optional, zeigt X-Button
  icon?: ReactNode           // optional, Icon links
}
```

**Variants:**
- **Primary:** Fett, farbiger Hintergrund
- **Secondary:** Subtil, heller Hintergrund
- **Outline:** Nur Border, transparenter Hintergrund

**Styling:**
```css
/* Base */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem 0.5rem;
  border-radius: 12px;
  font-size: 0.75rem;
  font-weight: 500;
  white-space: nowrap;
}

/* Primary */
.chip.primary {
  background: var(--accent);
  color: white;
  font-weight: 600;
}

/* Secondary */
.chip.secondary {
  background: var(--surface2);
  color: var(--text2);
}

/* Sizes */
.chip.sm { padding: 0.125rem 0.375rem; font-size: 0.625rem; }
.chip.md { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
.chip.lg { padding: 0.375rem 0.75rem; font-size: 0.875rem; }
```

**Usage:**
```jsx
<Chip label="Karate" primary color="var(--accent)" />
<Chip label="Breitensport" icon={<UserIcon />} />
<Chip label="Selbstverteidigung" onDelete={() => remove()} />
```

---

### 1.2 MultiSelect

**Beschreibung:** M:N Auswahl mit Primary-Toggle

**Props:**
```typescript
{
  options: Array<{id: number, name: string, ...}>  // required
  value: Array<{id: number, is_primary: boolean}>  // required
  onChange: (value) => void                        // required
  primaryToggle?: boolean                          // default: false
  label?: string
  placeholder?: string
  searchable?: boolean                             // default: true wenn > 10 options
  maxHeight?: string                               // default: '300px'
}
```

**Behavior:**
- Click auf Option: Add/Remove
- Star-Icon: Toggle `is_primary` (nur wenn `primaryToggle = true`)
- Keyboard: Arrow-Keys für Navigation, Enter für Select, Escape für Close
- Search: Auto-aktiviert wenn > 10 Options

**Styling:**
```css
.multiselect {
  position: relative;
}

.multiselect__trigger {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
}

.multiselect__dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  max-height: 300px;
  overflow-y: auto;
  z-index: 100;
}

.multiselect__option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  cursor: pointer;
  transition: background 0.2s;
}

.multiselect__option:hover {
  background: var(--surface2);
}

.multiselect__option.selected {
  background: var(--accent);
  color: white;
}

.multiselect__option__primary-toggle {
  margin-left: auto;
  opacity: 0;
  transition: opacity 0.2s;
}

.multiselect__option.selected .multiselect__option__primary-toggle {
  opacity: 1;
}
```

**Usage:**
```jsx
<MultiSelect 
  label="Fokusbereiche"
  options={focusAreas}
  value={formData.focus_areas_multi}
  onChange={handleChange}
  primaryToggle
  placeholder="Fokusbereiche auswählen..."
/>
```

---

### 1.3 Accordion

**Beschreibung:** Aufklappbare Sektion

**Props:**
```typescript
{
  title: string | ReactNode       // required
  children: ReactNode             // required
  defaultOpen?: boolean           // default: false
  icon?: ReactNode                // optional, Icon links vom Titel
  onToggle?: (isOpen: boolean) => void
}
```

**Behavior:**
- Click auf Header: Toggle open/close
- Animierter Übergang (height transition)
- Accessibility: ARIA-Labels, Keyboard-Support

**Styling:**
```css
.accordion {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.accordion__header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem;
  background: var(--surface);
  cursor: pointer;
  transition: background 0.2s;
}

.accordion__header:hover {
  background: var(--surface2);
}

.accordion__header__icon {
  transition: transform 0.3s;
}

.accordion.open .accordion__header__icon {
  transform: rotate(180deg);
}

.accordion__content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease-in-out;
}

.accordion.open .accordion__content {
  max-height: 1000px; /* Large enough for content */
  padding: 1rem;
}
```

**Usage:**
```jsx
<Accordion title="🎯 Ziel der Übung" defaultOpen icon={<ChevronDown />}>
  <p>{exercise.goal}</p>
</Accordion>
```

---

### 1.4 DragDropList

**Beschreibung:** Reorder-Support via Drag & Drop

**Props:**
```typescript
{
  items: Array<{id: number, ...}>    // required
  onReorder: (newOrder: number[]) => void  // required
  renderItem: (item, index) => ReactNode   // required
  disabled?: boolean                 // default: false
}
```

**Behavior:**
- Drag & Drop für Reorder
- Visual Feedback während Drag (Placeholder, Ghost)
- Mobile: Touch-Support

**Library:** `react-beautiful-dnd` oder `@dnd-kit/sortable`

**Usage:**
```jsx
<DragDropList 
  items={variants}
  onReorder={handleReorder}
  renderItem={(variant, index) => (
    <VariantCard variant={variant} index={index} />
  )}
/>
```

---

## 2. Exercise-Specific Components

### 2.1 ExerciseCard (Grid-Item)

**Beschreibung:** Karten-Darstellung für Grid/List

**Props:**
```typescript
{
  exercise: Exercise               // required
  onClick?: () => void             // Detail-Ansicht öffnen
  onEdit?: () => void              // Edit-Seite öffnen
  onDelete?: () => void            // Delete-Aktion
  compact?: boolean                // default: false (kompakte Darstellung)
}
```

**Layout:**
```
┌─────────────────────────────────┐
│ Title                           │
│ [Karate] [club] [approved]      │ ← Badges
│                                 │
│ Summary text...                 │
│                                 │
│ 👥 8-12   ⏱️ 15-20 Min         │ ← Meta-Info
│                                 │
│ [Bearbeiten]      [Löschen]     │
└─────────────────────────────────┘
```

**Responsive:**
- Desktop: 3 Spalten (min-width: 320px, max-width: 400px)
- Tablet: 2 Spalten
- Mobile: 1 Spalte

**Styling:**
```css
.exercise-card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  transition: box-shadow 0.2s, transform 0.2s;
  cursor: pointer;
}

.exercise-card:hover {
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  transform: translateY(-2px);
}

.exercise-card__title {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0;
}

.exercise-card__badges {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.exercise-card__summary {
  color: var(--text2);
  font-size: 0.875rem;
  line-height: 1.5;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
}

.exercise-card__meta {
  display: flex;
  gap: 1rem;
  font-size: 0.875rem;
  color: var(--text2);
}

.exercise-card__actions {
  display: flex;
  gap: 0.5rem;
  margin-top: auto;
}
```

---

### 2.2 ExerciseQuickInfo (Summary-Box)

**Beschreibung:** Kompakte Info-Box für Übersicht

**Props:**
```typescript
{
  exercise: Exercise               // required
  variant?: Variant                // optional (zeigt Varianten-Daten)
}
```

**Layout:**
```
┌─────────────────────────────────┐
│ Summary                         │
│ ─────────────────────────────── │
│ ⏱️ 15-20 Min                    │
│ 👥 8-12 Personen                │
│ 👶 Kinder, Teenager             │
│ 🔧 Matten, Pratzen              │
└─────────────────────────────────┘
```

**Styling:**
```css
.exercise-quickinfo {
  padding: 1rem;
  background: var(--surface2);
  border-radius: 8px;
}

.exercise-quickinfo__summary {
  margin-bottom: 0.75rem;
  font-size: 0.875rem;
  line-height: 1.5;
}

.exercise-quickinfo__meta {
  display: grid;
  gap: 0.5rem;
  font-size: 0.875rem;
}

.exercise-quickinfo__meta-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.exercise-quickinfo__meta-item__icon {
  opacity: 0.7;
}
```

---

### 2.3 VariantCard

**Beschreibung:** Darstellung einer Übungs-Variante

**Props:**
```typescript
{
  variant: Variant                 // required
  exercise: Exercise               // parent (für Kontext)
  onEdit?: () => void
  onDelete?: () => void
  draggable?: boolean              // default: false (für Reorder)
}
```

**Layout:**
```
┌─────────────────────────────────┐
│ ⭐ Level 2: Mittel              │ ← progression_level + primary
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ Description...                  │
│                                 │
│ Abweichungen:                   │
│ • Dauer: 10-15 Min              │
│ • Equipment: + Pratzen          │
│ • Schwierigkeit: Schwerer       │
│ ─────────────────────────────── │
│ [Bearbeiten]      [Löschen]     │
└─────────────────────────────────┘
```

**Styling:**
```css
.variant-card {
  padding: 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent); /* Primary indicator */
  border-radius: 8px;
}

.variant-card__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  font-weight: 600;
}

.variant-card__level {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem 0.5rem;
  background: var(--surface2);
  border-radius: 4px;
  font-size: 0.75rem;
}

.variant-card__differences {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

.variant-card__differences ul {
  margin: 0;
  padding-left: 1.5rem;
}
```

---

### 2.4 MediaGallery

**Beschreibung:** Medien-Anzeige (Bilder + Videos + Embeds)

**Props:**
```typescript
{
  media: Array<Media>              // required
  onMediaClick?: (media) => void   // optional (Lightbox öffnen)
  layout?: 'grid' | 'carousel'     // default: 'grid'
}
```

**Features:**
- Grid-Layout (default) oder Carousel
- Lightbox für Bilder (Click zum Vergrößern)
- Video-Player für lokale Videos
- iframe-Embed für YouTube/Instagram/Vimeo
- Primary-Media hervorheben

**Styling:**
```css
.media-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}

.media-gallery__item {
  position: relative;
  aspect-ratio: 16/9;
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
}

.media-gallery__item.primary {
  grid-column: span 2; /* Primary nimmt 2 Spalten */
}

.media-gallery__item img,
.media-gallery__item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.media-gallery__item__overlay {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 0.5rem;
  background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
  color: white;
  font-size: 0.75rem;
}
```

---

### 2.5 MediaUploader

**Beschreibung:** Upload-UI (Drag & Drop + Embed)

**Props:**
```typescript
{
  value: Array<Media>              // required (current media)
  onChange: (media) => void        // required
  onUpload: (file) => Promise<Media>      // Upload-Handler
  onEmbed: (url) => Promise<Media>        // Embed-Handler
  maxFiles?: number                // default: 10
  accept?: string                  // MIME-Types (default: 'image/*,video/*')
}
```

**Features:**
- **Tab 1 (Upload):** Drag & Drop Zone + File-Picker
- **Tab 2 (Embed):** URL-Input + Preview
- Reorder via Drag & Drop
- Delete-Button pro Medium
- Primary-Toggle

**Layout:**
```
┌─────────────────────────────────┐
│ [Upload] [Embed]                │ ← Tabs
│ ─────────────────────────────── │
│ [Drag & Drop Zone]              │
│ oder Klicken zum Auswählen      │
│ ─────────────────────────────── │
│ Hochgeladene Medien:            │
│ ┌─────┐ ┌─────┐ ┌─────┐        │
│ │ IMG │ │ VID │ │ EMB │        │
│ │ ⭐  │ │     │ │     │        │ ← Primary-Toggle
│ │ [x] │ │ [x] │ │ [x] │        │ ← Delete
│ └─────┘ └─────┘ └─────┘        │
└─────────────────────────────────┘
```

**Styling:**
```css
.media-uploader {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.media-uploader__tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
}

.media-uploader__tab {
  flex: 1;
  padding: 0.75rem;
  text-align: center;
  background: var(--surface2);
  cursor: pointer;
  transition: background 0.2s;
}

.media-uploader__tab.active {
  background: var(--surface);
  border-bottom: 2px solid var(--accent);
}

.media-uploader__dropzone {
  padding: 2rem;
  text-align: center;
  border: 2px dashed var(--border);
  border-radius: 8px;
  margin: 1rem;
  background: var(--surface2);
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
}

.media-uploader__dropzone:hover,
.media-uploader__dropzone.dragover {
  border-color: var(--accent);
  background: var(--surface);
}

.media-uploader__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 0.5rem;
  padding: 1rem;
}

.media-uploader__item {
  position: relative;
  aspect-ratio: 1;
  border-radius: 4px;
  overflow: hidden;
}

.media-uploader__item__actions {
  position: absolute;
  top: 0.25rem;
  right: 0.25rem;
  display: flex;
  gap: 0.25rem;
}

.media-uploader__item__primary-toggle,
.media-uploader__item__delete {
  padding: 0.25rem;
  background: rgba(0,0,0,0.7);
  border-radius: 4px;
  color: white;
  cursor: pointer;
}
```

---

## 3. Layout-Patterns

### 3.1 Detail-Page Layout

```
┌─────────────────────────────────┐
│ Header (sticky)                 │
│ - Title                         │
│ - Badges                        │
│ - Actions (Edit, Delete, etc.)  │
│ ─────────────────────────────── │
│ QuickInfo-Box (sticky?)         │
│ ─────────────────────────────── │
│ Section: Ziel                   │ ← Accordion
│ Section: Durchführung           │ ← Accordion (defaultOpen)
│ Section: Vorbereitung           │ ← Accordion
│ Section: Hinweise               │ ← Accordion
│ Section: Varianten              │ ← Accordion
│ Section: Fähigkeiten            │ ← Accordion
│ Section: Zuordnungen            │ ← Accordion
│ ─────────────────────────────── │
│ Footer (Actions wiederholen?)   │
└─────────────────────────────────┘
```

**Responsive:**
- Desktop (> 1024px): Sticky QuickInfo rechts (2-Column Layout)
- Tablet/Mobile: Alles vertikal

---

### 3.2 Grid-Layout

```css
/* Grid */
.exercises-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1rem;
}

/* Responsive Breakpoints */
@media (max-width: 768px) {
  .exercises-grid {
    grid-template-columns: 1fr; /* 1 Spalte auf Mobile */
  }
}

@media (min-width: 768px) and (max-width: 1024px) {
  .exercises-grid {
    grid-template-columns: repeat(2, 1fr); /* 2 Spalten auf Tablet */
  }
}

@media (min-width: 1024px) {
  .exercises-grid {
    grid-template-columns: repeat(3, 1fr); /* 3 Spalten auf Desktop */
  }
}
```

---

### 3.3 Form-Layout

```css
/* Desktop: 2-Column Grid */
@media (min-width: 768px) {
  .exercise-form {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1rem;
    align-items: start;
  }
  
  .exercise-form__label {
    text-align: right;
    padding-top: 0.75rem; /* Align with input */
  }
}

/* Mobile: 1-Column (Labels above) */
@media (max-width: 767px) {
  .exercise-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
  
  .exercise-form__row {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }
}
```

---

## 4. Responsive Breakpoints

```css
:root {
  --mobile: 0px;
  --tablet: 768px;
  --desktop: 1024px;
  --wide: 1440px;
}

/* Media Queries */
@media (min-width: 768px) { /* Tablet */ }
@media (min-width: 1024px) { /* Desktop */ }
@media (min-width: 1440px) { /* Wide */ }
```

---

## 5. Styling-Konventionen

### 5.1 CSS Variables (aus app.css)

```css
/* Farben */
--accent: #1D9E75
--accent-dark: #085041
--danger: #D85A30

/* Graustufen */
--bg: #...
--surface: #...
--surface2: #...
--border: #...
--text1: #...
--text2: #...
--text3: #...
```

---

### 5.2 Spacing-Scale

```css
--spacing-xs: 0.25rem  /* 4px */
--spacing-sm: 0.5rem   /* 8px */
--spacing-md: 1rem     /* 16px */
--spacing-lg: 1.5rem   /* 24px */
--spacing-xl: 2rem     /* 32px */
--spacing-2xl: 3rem    /* 48px */
```

---

### 5.3 Typography

```css
h1 { font-size: 2rem; font-weight: 700; }      /* 32px */
h2 { font-size: 1.5rem; font-weight: 600; }    /* 24px */
h3 { font-size: 1.25rem; font-weight: 600; }   /* 20px */
body { font-size: 1rem; font-weight: 400; }    /* 16px */
small { font-size: 0.875rem; font-weight: 400; } /* 14px */
```

---

## 6. Accessibility

### 6.1 ARIA-Labels

```jsx
// Buttons ohne Text
<button aria-label="Übung bearbeiten">
  <EditIcon />
</button>

// Inputs mit Label
<label htmlFor="title">Titel</label>
<input id="title" type="text" />

// Modals
<div role="dialog" aria-labelledby="modal-title">
  <h2 id="modal-title">Übung bearbeiten</h2>
</div>
```

---

### 6.2 Keyboard Navigation

- **Tab:** Fokus-Reihenfolge logisch (top → bottom, left → right)
- **Enter/Space:** Aktionen ausführen
- **Escape:** Modals/Dropdowns schließen
- **Arrow-Keys:** Listen/Grids navigieren

---

### 6.3 Focus-Styles

```css
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

button:focus-visible,
a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
```

---

---

## 2.6 BlockEditor

**Beschreibung:** Editor für Exercise Blocks – fügt Übungen in Reihenfolge zusammen,
optional mit Template-Modus für Platzhalter.

**Props:**
```typescript
{
  value: ExerciseBlock               // required
  onChange: (block) => void          // required
  onAddExercise: () => void          // Öffnet Exercise-Picker
  onAddPlaceholder: () => void       // Öffnet Placeholder-Form
  readOnly?: boolean                 // default: false
  showTemplatControls?: boolean      // default: false (nur im Template-Modus)
}
```

**Layout:**

```
┌─────────────────────────────────────────┐
│ Block-Name [Bearbeiten]                 │
│ "Aufwärmblock Kumite" · 4 Übungen      │
│ ─────────────────────────────────────── │
│ [= 1] Maai - Distanzübung              │ ← DragHandle + Position
│       5-10 min · Karate · 2 Varianten  │
│       [Variante wählen ▼] [Notiz] [✕]  │
│ ─────────────────────────────────────── │
│ [= 2] Kizami-Zuki Kihon                │
│       10-15 min · Karate               │
│       [Notiz] [✕]                      │
│ ─────────────────────────────────────── │
│ [= 3] ⬜ PLATZHALTER                   │ ← Placeholder (Template-Modus)
│       "Schlag-Übung" · max. 10 min     │
│       [Kriterien bearbeiten] [✕]       │
│ ─────────────────────────────────────── │
│ [+ Übung hinzufügen] [+ Platzhalter]   │
└─────────────────────────────────────────┘
```

**Features:**
- Drag & Drop Reordering (via `DragDropList`)
- Varianten-Dropdown pro Item (falls Exercise Varianten hat)
- Notiz-Feld pro Item (expandierbar)
- Platzhalter (nur bei is_template=true im Block aktiv)
- Platzhalter-Kriterien in eigenem Dialog spezifizieren
- ReadOnly-Modus für Ansicht ohne Bearbeitung

**Platzhalter-Dialog:**
```
┌─────────────────────────────────┐
│ Platzhalter konfigurieren       │
│ ─────────────────────────────── │
│ Label: [Schlag-Übung (10min)]   │
│ ─────────────────────────────── │
│ Kriterien (alle optional):      │
│ Fokusbereich: [Karate ▼]        │
│ Max. Dauer: [10] min            │
│ Min. Dauer: [5]  min            │
│ Schwierigkeit: [Gleich ▼]       │
│ ─────────────────────────────── │
│ [Abbrechen]          [Speichern]│
└─────────────────────────────────┘
```

**Styling:**
```css
.block-editor {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.block-editor__item {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.block-editor__item--placeholder {
  background: var(--surface2);
  border-left: 3px solid var(--accent);
  opacity: 0.85;
}

.block-editor__item:last-child {
  border-bottom: none;
}

.block-editor__drag-handle {
  cursor: grab;
  color: var(--text3);
  padding: 0.25rem;
}

.block-editor__position {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text2);
  min-width: 1.5rem;
  padding-top: 0.1rem;
}

.block-editor__item-content {
  flex: 1;
}

.block-editor__item-title {
  font-weight: 500;
  color: var(--text1);
}

.block-editor__item-meta {
  font-size: 0.8rem;
  color: var(--text2);
  margin-top: 0.25rem;
}

.block-editor__item-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}

.block-editor__add-actions {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  background: var(--surface2);
}
```

---

**Version:** 1.1
**Letzte Änderung:** 2026-04-24
**Status:** REVIEWED - Pending Implementation
**Review-Änderungen:** BlockEditor Komponente (§2.6) hinzugefügt
