Enhance Exercise Form UI and Functionality with New Tab Structure
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 15s
Test Suite / k6 /health Baseline (pull_request) Successful in 34s
Test Suite / playwright-tests (pull_request) Successful in 1m15s

- Introduced a tabbed interface for the exercise form, allowing users to navigate between different sections (Stammdaten, Anleitung, Einordnung, etc.) more intuitively.
- Added new CSS styles for the exercise form, including improved layout and visual differentiation for various sections.
- Implemented dynamic tab management based on exercise type and edit state, enhancing user experience during form interactions.
- Refactored existing components to integrate the new tab structure, ensuring a cohesive design and functionality across the exercise form.
This commit is contained in:
Lars 2026-05-21 14:58:14 +02:00
parent 7f62b6ceee
commit 13a1d3a060
3 changed files with 384 additions and 174 deletions

View File

@ -6548,9 +6548,110 @@ html.modal-scroll-locked .app-main {
min-width: 0; min-width: 0;
} }
/* Übungsformular — Register-Tabs & farbige Bereiche */
.exercise-form-edit {
padding-top: 4px;
}
.exercise-form-edit__tabbar {
display: flex;
align-items: stretch;
margin: 0 0 16px;
padding: 0 0 12px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 6;
background: var(--surface);
}
.exercise-form-edit__tabbar .admin-page-subtabs {
flex: 1;
min-width: 0;
}
.exercise-form-panel {
padding: 4px 0 8px 14px;
margin-bottom: 4px;
border-left: 3px solid var(--border);
}
.exercise-form-panel--basics {
border-left-color: var(--accent);
}
.exercise-form-panel--guide {
border-left-color: color-mix(in srgb, #2563eb 70%, var(--accent));
}
.exercise-form-panel--classify {
border-left-color: color-mix(in srgb, #7c3aed 65%, var(--accent));
}
.exercise-form-panel--combo {
border-left-color: color-mix(in srgb, #d97706 70%, var(--accent-dark));
}
.exercise-form-panel--variants {
border-left-color: color-mix(in srgb, #0891b2 70%, var(--accent));
}
.exercise-form-panel--media {
border-left-color: color-mix(in srgb, var(--text3) 55%, var(--border));
}
.exercise-form-panel__title {
margin: 0 0 4px;
font-size: 1.05rem;
font-weight: 700;
}
.exercise-form-panel__hint {
margin: 0 0 14px;
font-size: 12px;
color: var(--text3);
line-height: 1.45;
}
.exercise-form-panel__body {
min-width: 0;
}
.exercise-form-type-box {
padding: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--surface2);
margin-bottom: 12px;
}
.exercise-form-type-box__hint {
margin: 8px 0 0;
font-size: 12px;
color: var(--text2);
line-height: 1.45;
}
.exercise-form-inline-tab-link {
padding: 0;
border: none;
background: none;
color: var(--accent-dark);
font: inherit;
font-weight: 700;
text-decoration: underline;
cursor: pointer;
}
.exercise-form-subsection {
padding: 12px 0;
border-top: 1px dashed var(--border);
margin-top: 8px;
}
.exercise-form-subsection:first-child {
border-top: none;
margin-top: 0;
padding-top: 0;
}
.exercise-form-subsection__title {
margin: 0 0 6px;
font-size: 0.95rem;
font-weight: 700;
}
.exercise-form-subsection__hint {
margin: 0 0 10px;
font-size: 12px;
color: var(--text3);
line-height: 1.4;
}
/* Übungsformular — Klassifikation & Meta-Chips */ /* Übungsformular — Klassifikation & Meta-Chips */
.exercise-form-meta-panel { .exercise-form-meta-panel {
margin: 4px 0 16px; margin: 0;
padding: 14px; padding: 14px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;

View File

@ -0,0 +1,32 @@
import React from 'react'
import PageSectionNav from '../PageSectionNav'
export function ExerciseFormTabBar({ activeTab, onChange, items }) {
return (
<div className="exercise-form-edit__tabbar">
<PageSectionNav
ariaLabel="Übungsbereiche"
value={activeTab}
onChange={onChange}
items={items}
className="page-section-nav--embedded exercise-form-edit__section-nav"
/>
</div>
)
}
export function ExerciseFormPanel({ tab, activeTab, tone = 'default', title, hint, children }) {
if (activeTab !== tab) return null
return (
<section
id={`exercise-form-panel-${tab}`}
className={`exercise-form-panel exercise-form-panel--${tone}`}
role="tabpanel"
aria-labelledby={`exercise-form-tab-${tab}`}
>
{title ? <h3 className="exercise-form-panel__title">{title}</h3> : null}
{hint ? <p className="exercise-form-panel__hint">{hint}</p> : null}
<div className="exercise-form-panel__body">{children}</div>
</section>
)
}

View File

@ -26,9 +26,10 @@ import {
} from '../../utils/activeClub' } from '../../utils/activeClub'
import { COMBINATION_ARCHETYPE_OPTIONS, ARCHETYPE_DEFAULT_REP_SERIES_COUNT, defaultRepSeriesCountForArchetype } from '../../constants/combinationArchetypes' import { COMBINATION_ARCHETYPE_OPTIONS, ARCHETYPE_DEFAULT_REP_SERIES_COUNT, defaultRepSeriesCountForArchetype } from '../../constants/combinationArchetypes'
import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../../utils/combinationMethodProfileUi' import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../../utils/combinationMethodProfileUi'
import { GripVertical } from 'lucide-react' import { GripVertical, FileText, BookOpen, Tags, Layers, GitBranch, Image as ImageIcon } from 'lucide-react'
import UnsavedChangesPrompt from '../UnsavedChangesPrompt' import UnsavedChangesPrompt from '../UnsavedChangesPrompt'
import PageFormEditorChrome from '../PageFormEditorChrome' import PageFormEditorChrome from '../PageFormEditorChrome'
import { ExerciseFormTabBar, ExerciseFormPanel } from './ExerciseFormLayout'
import { useNavReturn } from '../../hooks/useNavReturn' import { useNavReturn } from '../../hooks/useNavReturn'
import { import {
EXERCISES_LIST_PATH, EXERCISES_LIST_PATH,
@ -499,9 +500,45 @@ function ExerciseFormPageRoot() {
const [variantSavingId, setVariantSavingId] = useState(null) const [variantSavingId, setVariantSavingId] = useState(null)
const [variantBusy, setVariantBusy] = useState(false) const [variantBusy, setVariantBusy] = useState(false)
const [variantEditSelection, setVariantEditSelection] = useState(null) const [variantEditSelection, setVariantEditSelection] = useState(null)
const variantsDetailsRef = useRef(null) const [activeFormTab, setActiveFormTab] = useState('stammdaten')
const variantsSavedSnapshotRef = useRef({}) const variantsSavedSnapshotRef = useRef({})
const exerciseFormTabs = useMemo(() => {
const tabs = [
{ id: 'stammdaten', label: 'Stammdaten', icon: FileText },
{ id: 'anleitung', label: 'Anleitung', icon: BookOpen },
{ id: 'einordnung', label: 'Einordnung', icon: Tags },
]
if (formData.exercise_kind === 'combination') {
tabs.push({ id: 'kombination', label: 'Kombination', icon: Layers })
}
if (isEdit) {
if (formData.exercise_kind !== 'combination') {
tabs.push({
id: 'varianten',
label: variants.length > 0 ? `Varianten (${variants.length})` : 'Varianten',
icon: GitBranch,
})
}
tabs.push({ id: 'medien', label: 'Medien & Mehr', icon: ImageIcon })
} else {
tabs.push({ id: 'varianten', label: 'Varianten', icon: GitBranch, disabled: true })
tabs.push({ id: 'medien', label: 'Medien & Mehr', icon: ImageIcon, disabled: true })
}
return tabs
}, [formData.exercise_kind, isEdit, variants.length])
useEffect(() => {
const allowed = new Set(exerciseFormTabs.filter((t) => !t.disabled).map((t) => t.id))
if (!allowed.has(activeFormTab)) setActiveFormTab('stammdaten')
}, [exerciseFormTabs, activeFormTab])
useEffect(() => {
if (formData.exercise_kind === 'combination' && activeFormTab === 'varianten') {
setActiveFormTab('kombination')
}
}, [formData.exercise_kind, activeFormTab])
const syncVariantsSavedSnapshot = useCallback((rows) => { const syncVariantsSavedSnapshot = useCallback((rows) => {
const snap = {} const snap = {}
for (const v of rows || []) { for (const v of rows || []) {
@ -657,10 +694,10 @@ function ExerciseFormPageRoot() {
}, [variants, variantEditSelection]) }, [variants, variantEditSelection])
useEffect(() => { useEffect(() => {
if (variantEditSelection != null && variantsDetailsRef.current) { if (variantEditSelection != null && isEdit && formData.exercise_kind !== 'combination') {
variantsDetailsRef.current.open = true setActiveFormTab('varianten')
} }
}, [variantEditSelection]) }, [variantEditSelection, isEdit, formData.exercise_kind])
const updateFormField = (field, value) => { const updateFormField = (field, value) => {
setFormDirty(true) setFormDirty(true)
@ -1246,8 +1283,25 @@ function ExerciseFormPageRoot() {
</p> </p>
) : null} ) : null}
<div className="card"> <div className="card exercise-form-edit">
<form id="exercise-form" onSubmit={handleSubmit}> <form id="exercise-form" onSubmit={handleSubmit}>
<ExerciseFormTabBar
activeTab={activeFormTab}
onChange={setActiveFormTab}
items={exerciseFormTabs}
/>
<ExerciseFormPanel
tab="stammdaten"
activeTab={activeFormTab}
tone="basics"
title="Stammdaten"
hint={
isEdit
? 'Titel, Rahmendaten und Sichtbarkeit — Inhalt und Einordnung in den anderen Tabs.'
: 'Titel und Rahmendaten. Varianten, Medien und Progressionsgraph sind nach dem ersten Speichern verfügbar.'
}
>
<div className="form-row"> <div className="form-row">
<label className="form-label">Titel *</label> <label className="form-label">Titel *</label>
<input <input
@ -1273,16 +1327,7 @@ function ExerciseFormPageRoot() {
/> />
</div> </div>
<div <div className="exercise-form-type-box">
style={{
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--surface2)',
marginBottom: '12px',
}}
>
<h3 style={{ marginTop: 0, marginBottom: '10px', fontSize: '1rem' }}>Übungstyp</h3>
<div className="form-row"> <div className="form-row">
<label className="form-label">Art</label> <label className="form-label">Art</label>
<select <select
@ -1302,12 +1347,144 @@ function ExerciseFormPageRoot() {
} }
: {}), : {}),
})) }))
if (nk === 'combination') setActiveFormTab('kombination')
}} }}
> >
<option value="simple">Einzelübung</option> <option value="simple">Einzelübung</option>
<option value="combination">Kombinationsübung (Stationen / Pool)</option> <option value="combination">Kombinationsübung (Stationen / Pool)</option>
</select> </select>
</div> </div>
{formData.exercise_kind === 'combination' ? (
<p className="exercise-form-type-box__hint">
Stationen und Ablaufprofil im Tab{' '}
<button type="button" className="exercise-form-inline-tab-link" onClick={() => setActiveFormTab('kombination')}>
Kombination
</button>
.
</p>
) : null}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<div className="form-row">
<label className="form-label">Dauer Min</label>
<input
type="number"
className="form-input"
value={formData.duration_min}
onChange={(e) =>
updateFormField('duration_min', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
<div className="form-row">
<label className="form-label">Dauer Max</label>
<input
type="number"
className="form-input"
value={formData.duration_max}
onChange={(e) =>
updateFormField('duration_max', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<div className="form-row">
<label className="form-label">Gruppe Min</label>
<input
type="number"
className="form-input"
value={formData.group_size_min}
onChange={(e) =>
updateFormField('group_size_min', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
<div className="form-row">
<label className="form-label">Gruppe Max</label>
<input
type="number"
className="form-input"
value={formData.group_size_max}
onChange={(e) =>
updateFormField('group_size_max', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
</div>
<div className="form-row">
<label className="form-label">Material (eine Zeile oder kommagetrennt)</label>
<textarea
className="form-input"
rows={3}
value={formData.equipmentLines}
onChange={(e) => updateFormField('equipmentLines', e.target.value)}
placeholder="Matten&#10;Pratzen"
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<div className="form-row">
<label className="form-label">Sichtbarkeit</label>
<select
className="form-input"
value={formData.visibility}
onChange={(e) => updateFormField('visibility', e.target.value)}
>
<option value="private">Privat</option>
<option value="club">Verein</option>
{isSuperadmin ? <option value="official">Offiziell</option> : null}
</select>
</div>
<div className="form-row">
<label className="form-label">Status</label>
<select
className="form-input"
value={formData.status}
onChange={(e) => updateFormField('status', e.target.value)}
>
<option value="draft">Entwurf</option>
<option value="in_review">In Prüfung</option>
<option value="approved">Freigegeben</option>
<option value="archived">Archiviert</option>
</select>
</div>
</div>
{formData.visibility === 'club' && visibilityClubChoices.length > 0 ? (
<div className="form-row" style={{ marginTop: '10px' }}>
<label className="form-label">Verein (Sichtbarkeit)</label>
<select
className="form-input"
value={formData.club_id != null && formData.club_id !== '' ? String(formData.club_id) : ''}
onChange={(e) => {
const v = e.target.value
updateFormField('club_id', v === '' ? null : Number(v))
}}
>
{visibilityClubChoices.map((c) => (
<option key={c.id} value={String(c.id)}>
{(c.name || '').trim() || `Verein #${c.id}`}
</option>
))}
</select>
<p style={{ margin: '6px 0 0', fontSize: '12px', color: 'var(--text3)', lineHeight: 1.4 }}>
Standard ist der aktive Verein aus der Navigation. Bei Plattform-Admins sind alle Vereine wählbar.
</p>
</div>
) : null}
</ExerciseFormPanel>
<ExerciseFormPanel
tab="kombination"
activeTab={activeFormTab}
tone="combo"
title="Kombinationsübung"
hint="Stationen, Übungs-Pools und globales Ablaufprofil für Coach und Planung."
>
{formData.exercise_kind === 'combination' ? ( {formData.exercise_kind === 'combination' ? (
<> <>
<div className="form-row"> <div className="form-row">
@ -1721,9 +1898,20 @@ function ExerciseFormPageRoot() {
/> />
</div> </div>
</> </>
) : null} ) : (
</div> <p className="exercise-form-panel__hint" style={{ margin: 0 }}>
Wähle unter <strong>Stammdaten</strong> die Art Kombinationsübung, um Stationen zu planen.
</p>
)}
</ExerciseFormPanel>
<ExerciseFormPanel
tab="anleitung"
activeTab={activeFormTab}
tone="guide"
title="Anleitung"
hint="Ziel, Ablauf und Hinweise — Medien kannst du in die Texte einbetten (Symbolleiste)."
>
<div className="form-row"> <div className="form-row">
<label className="form-label">Ziel *</label> <label className="form-label">Ziel *</label>
<RichTextEditor <RichTextEditor
@ -1775,72 +1963,16 @@ function ExerciseFormPageRoot() {
onExerciseMediaListChanged={refreshMedia} onExerciseMediaListChanged={refreshMedia}
/> />
</div> </div>
</ExerciseFormPanel>
<div className="form-row"> <ExerciseFormPanel
<label className="form-label">Material (eine Zeile oder kommagetrennt)</label> tab="einordnung"
<textarea activeTab={activeFormTab}
className="form-input" tone="classify"
rows={3} title="Einordnung"
value={formData.equipmentLines} hint="Fokus, Stile, Zielgruppen und Fähigkeiten für Suche, Filter und Skill-Profil."
onChange={(e) => updateFormField('equipmentLines', e.target.value)} >
placeholder="Matten&#10;Pratzen" <section className="exercise-form-meta-panel" aria-label="Klassifikation">
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<div className="form-row">
<label className="form-label">Dauer Min</label>
<input
type="number"
className="form-input"
value={formData.duration_min}
onChange={(e) =>
updateFormField('duration_min', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
<div className="form-row">
<label className="form-label">Dauer Max</label>
<input
type="number"
className="form-input"
value={formData.duration_max}
onChange={(e) =>
updateFormField('duration_max', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<div className="form-row">
<label className="form-label">Gruppe Min</label>
<input
type="number"
className="form-input"
value={formData.group_size_min}
onChange={(e) =>
updateFormField('group_size_min', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
<div className="form-row">
<label className="form-label">Gruppe Max</label>
<input
type="number"
className="form-input"
value={formData.group_size_max}
onChange={(e) =>
updateFormField('group_size_max', e.target.value ? parseInt(e.target.value, 10) : '')
}
/>
</div>
</div>
<section className="exercise-form-meta-panel" aria-labelledby="exercise-meta-heading">
<h3 id="exercise-meta-heading" className="exercise-form-meta-panel__title">
Klassifikation &amp; Zielgruppe
</h3>
<div className="exercise-form-meta-panel__grid"> <div className="exercise-form-meta-panel__grid">
<ExerciseCatalogAssocEditor <ExerciseCatalogAssocEditor
title="Fokusbereiche" title="Fokusbereiche"
@ -1893,77 +2025,16 @@ function ExerciseFormPageRoot() {
onUpdateField={updateSkillField} onUpdateField={updateSkillField}
/> />
</section> </section>
</ExerciseFormPanel>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}> {isEdit && formData.exercise_kind !== 'combination' ? (
<div className="form-row"> <ExerciseFormPanel
<label className="form-label">Sichtbarkeit</label> tab="varianten"
<select activeTab={activeFormTab}
className="form-input" tone="variants"
value={formData.visibility} title="Übungsvarianten"
onChange={(e) => updateFormField('visibility', e.target.value)} hint="Pro Durchgang eine Variante. Änderungen werden mit Speichern in der Aktionsleiste mitgesichert."
> >
<option value="private">Privat</option>
<option value="club">Verein</option>
{isSuperadmin ? <option value="official">Offiziell</option> : null}
</select>
</div>
<div className="form-row">
<label className="form-label">Status</label>
<select
className="form-input"
value={formData.status}
onChange={(e) => updateFormField('status', e.target.value)}
>
<option value="draft">Entwurf</option>
<option value="in_review">In Prüfung</option>
<option value="approved">Freigegeben</option>
<option value="archived">Archiviert</option>
</select>
</div>
</div>
{formData.visibility === 'club' && visibilityClubChoices.length > 0 ? (
<div className="form-row" style={{ marginTop: '10px' }}>
<label className="form-label">Verein (Sichtbarkeit)</label>
<select
className="form-input"
value={formData.club_id != null && formData.club_id !== '' ? String(formData.club_id) : ''}
onChange={(e) => {
const v = e.target.value
updateFormField('club_id', v === '' ? null : Number(v))
}}
>
{visibilityClubChoices.map((c) => (
<option key={c.id} value={String(c.id)}>
{(c.name || '').trim() || `Verein #${c.id}`}
</option>
))}
</select>
<p style={{ margin: '6px 0 0', fontSize: '12px', color: 'var(--text3)', lineHeight: 1.4 }}>
Standard ist der aktive Verein aus der Navigation. Bei Plattform-Admins sind alle Vereine wählbar.
</p>
</div>
) : null}
</form>
</div>
{isEdit && formData.exercise_kind !== 'combination' && (
<details ref={variantsDetailsRef} className="card exercise-variants-details" style={{ marginTop: '16px' }}>
<summary className="exercise-variants-summary">
<span className="exercise-variants-summary__title">Übungsvarianten</span>
<span className="exercise-variants-summary__badge">
{variants.length === 0
? 'keine'
: `${variants.length} ${variants.length === 1 ? 'Variante' : 'Varianten'}`}
</span>
</summary>
<div className="exercise-variants-details__body">
<p className="exercise-variants-hint">
Pro Durchgang nur eine Variante bearbeiten weniger Scrollen. Reihenfolge entspricht Planung und Auswahl im
Training; Voraussetzung nutzt ihr später für Progressions-Serien. Offene Varianten-Änderungen werden mit
Speichern in der Aktionsleiste (oder in der Sicherheitsabfrage beim Verlassen) automatisch mitgespeichert.
</p>
{variants.length > 0 && ( {variants.length > 0 && (
<div className="form-row"> <div className="form-row">
<label className="form-label" htmlFor="variant-edit-select"> <label className="form-label" htmlFor="variant-edit-select">
@ -2106,25 +2177,19 @@ function ExerciseFormPageRoot() {
Wähle eine Variante zum Bearbeiten oder Neue Variante anlegen. Wähle eine Variante zum Bearbeiten oder Neue Variante anlegen.
</p> </p>
)} )}
</div> </ExerciseFormPanel>
</details> ) : null}
)}
{isEdit && formData.exercise_kind !== 'combination' && ( {isEdit ? (
<details className="card exercise-variants-details" style={{ marginTop: '16px' }}> <ExerciseFormPanel
<summary className="exercise-variants-summary"> tab="medien"
<span className="exercise-variants-summary__title">Progressionsgraph</span> activeTab={activeFormTab}
<span className="exercise-variants-summary__badge">Übung Übung</span> tone="media"
</summary> title="Medien & Erweiterungen"
<div className="exercise-variants-details__body"> hint="Verknüpfte Dateien, Progressionsgraph und Medienarchiv."
<ExerciseProgressionGraphPanel anchorExerciseId={exerciseId} anchorTitle={formData.title} /> >
</div> <div className="exercise-form-subsection exercise-form-subsection--media">
</details> <h4 className="exercise-form-subsection__title">Medien</h4>
)}
{isEdit && (
<div className="card" style={{ marginTop: '16px' }}>
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Medien</h2>
<p style={{ color: 'var(--text2)', fontSize: '13px', marginBottom: '6px' }}> <p style={{ color: 'var(--text2)', fontSize: '13px', marginBottom: '6px' }}>
Neue Uploads oder Embeds über die Textfeld-Symbolleiste (Medien im Text / Embed im Text). Hier Neue Uploads oder Embeds über die Textfeld-Symbolleiste (Medien im Text / Embed im Text). Hier
verwaltest du Verknüpfungen Kachel in ein Textfeld ziehen, um sie an der Cursorposition einzufügen verwaltest du Verknüpfungen Kachel in ein Textfeld ziehen, um sie an der Cursorposition einzufügen
@ -2268,6 +2333,15 @@ function ExerciseFormPageRoot() {
Verknüpfungen bleiben nötig (u. a. Zugriff, Orphan-Hinweise): Im Fließtext verweist du gezielt über Verknüpfungen bleiben nötig (u. a. Zugriff, Orphan-Hinweise): Im Fließtext verweist du gezielt über
Platzhalter. Ohne Verknüpfung gäbe es keine exercise_media-ID zum Einbetten. Platzhalter. Ohne Verknüpfung gäbe es keine exercise_media-ID zum Einbetten.
</p> </p>
</div>
{formData.exercise_kind !== 'combination' ? (
<div className="exercise-form-subsection exercise-form-subsection--graph">
<h4 className="exercise-form-subsection__title">Progressionsgraph</h4>
<p className="exercise-form-subsection__hint">Übergänge zu anderen Übungen für Progressions-Serien.</p>
<ExerciseProgressionGraphPanel anchorExerciseId={exerciseId} anchorTitle={formData.title} />
</div>
) : null}
{archiveOpen && ( {archiveOpen && (
<div <div
role="dialog" role="dialog"
@ -2408,8 +2482,11 @@ function ExerciseFormPageRoot() {
onClose={() => setReportTarget(null)} onClose={() => setReportTarget(null)}
/> />
)} )}
</div> </ExerciseFormPanel>
)} ) : null}
</form>
</div>
<ExercisePickerModal <ExercisePickerModal
open={comboStationPickerIx !== null} open={comboStationPickerIx !== null}