# 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 } /> 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 ``` --- ### 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 }>

{exercise.goal}

``` --- ### 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 ( )} /> ``` --- ## 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 // 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 // required (current media) onChange: (media) => void // required onUpload: (file) => Promise // Upload-Handler onEmbed: (url) => Promise // 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 // Inputs mit Label // Modals
``` --- ### 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