shinkan-jinkendo/.claude/docs/technical/UI_COMPONENTS_SPEC.md
Lars 6801c60604
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m55s
feat: Add MediaWiki import functionality with tracking and mapping
- 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.
2026-04-24 14:41:52 +02:00

1037 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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