1037 lines
23 KiB
Markdown
1037 lines
23 KiB
Markdown
|
|
# 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
|