- Implemented a new SQL migration for wiki import tracking tables. - Created an import router for handling MediaWiki imports of exercises, skills, and methods. - Developed a Semantic MediaWiki API client for direct API interactions. - Added a mapper to convert SMW properties to local database fields. - Introduced background tasks for asynchronous import processing. - Implemented logging and error handling for import operations. - Added endpoints for previewing imports, checking import status, and managing import references.
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
|