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

23 KiB
Raw Blame History

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 wenn primaryToggle = 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