- 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.
23 KiB
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:
{
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:
/* 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:
<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:
{
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 wennprimaryToggle = true) - Keyboard: Arrow-Keys für Navigation, Enter für Select, Escape für Close
- Search: Auto-aktiviert wenn > 10 Options
Styling:
.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:
<MultiSelect
label="Fokusbereiche"
options={focusAreas}
value={formData.focus_areas_multi}
onChange={handleChange}
primaryToggle
placeholder="Fokusbereiche auswählen..."
/>
1.3 Accordion
Beschreibung: Aufklappbare Sektion
Props:
{
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:
.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:
<Accordion title="🎯 Ziel der Übung" defaultOpen icon={<ChevronDown />}>
<p>{exercise.goal}</p>
</Accordion>
1.4 DragDropList
Beschreibung: Reorder-Support via Drag & Drop
Props:
{
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:
<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:
{
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:
.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:
{
exercise: Exercise // required
variant?: Variant // optional (zeigt Varianten-Daten)
}
Layout:
┌─────────────────────────────────┐
│ Summary │
│ ─────────────────────────────── │
│ ⏱️ 15-20 Min │
│ 👥 8-12 Personen │
│ 👶 Kinder, Teenager │
│ 🔧 Matten, Pratzen │
└─────────────────────────────────┘
Styling:
.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:
{
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:
.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:
{
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:
.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:
{
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:
.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
/* 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
/* 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
: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)
/* Farben */
--accent: #1D9E75
--accent-dark: #085041
--danger: #D85A30
/* Graustufen */
--bg: #...
--surface: #...
--surface2: #...
--border: #...
--text1: #...
--text2: #...
--text3: #...
5.2 Spacing-Scale
--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
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
// 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
: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:
{
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:
.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