From 930a7863155020448beed0edcd6ada7a189e08cf Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 14 May 2026 09:05:15 +0200 Subject: [PATCH] refactor(ui): enhance styling and structure of training unit sections and combination plan bracket - Updated CSS for training unit sections to improve layout and responsiveness, ensuring combo planning strips are displayed correctly. - Refactored CombinationPlanBracket component to accept additional class names for better customization. - Removed unused functions and streamlined imports in TrainingUnitSectionsEditor for cleaner code. - Reintroduced ExercisePickerModal with improved placement in ExerciseFormPage for better user experience. --- frontend/src/app.css | 70 ++++++- .../src/components/CombinationPlanBracket.jsx | 3 +- .../components/TrainingUnitSectionsEditor.jsx | 182 +++++------------- frontend/src/pages/ExerciseFormPage.jsx | 25 +-- 4 files changed, 126 insertions(+), 154 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index a5ea375..64c3d7a 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5414,22 +5414,80 @@ a.analysis-split__nav-item { 0 2px 12px rgba(15, 23, 42, 0.05); } -/* Kombinations‑Strip: volle Breite unter der Zeile, begrenzte Textbreite — Hauptzeile (Name/Min.) nicht verdrängen */ +/* Kombinationszeile: immer unter Hauptzeile (Titel / Minuten / Aktionen), nicht daneben */ +.training-unit-sections-editor .tu-item-row--exercise.tu-item-row--combo { + flex-direction: column; + align-items: stretch; + flex-wrap: nowrap; + gap: 0; +} + +.training-unit-sections-editor .tu-item-row--exercise.tu-item-row--combo .tu-item-row__mainline { + flex: none; + width: 100%; +} + +/* Kombinations‑Strip: volle Breite; oben „Ablauf bearbeiten“, darunter Klammer‑Vorschau */ .training-unit-sections-editor .tu-combo-planning-strip { display: flex; flex-direction: column; align-items: stretch; gap: 10px; + padding: 10px 12px 12px; + border-top: 1px solid color-mix(in srgb, var(--border2) 85%, var(--accent) 12%); + background: color-mix(in srgb, var(--surface2) 65%, var(--surface)); + margin-top: 2px; } -.training-unit-sections-editor .tu-combo-planning-strip__meta { - width: 100%; - max-width: min(100%, 42rem); +.training-unit-sections-editor--item-drag .tu-item-row--combo .tu-combo-planning-strip { + padding-left: 44px; +} + +.training-unit-sections-editor .tu-combo-planning-strip__toolbar { + display: flex; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.training-unit-sections-editor .tu-combo-planning-strip__meta--fallback { + font-size: 0.78rem; + color: var(--text2); + line-height: 1.45; +} + +.training-unit-sections-editor .tu-combo-planning-strip__bracket-wrap { min-width: 0; + overflow-x: auto; } -.training-unit-sections-editor .tu-combo-planning-strip > .btn { - align-self: flex-start; +.training-unit-sections-editor .combo-plan-bracket--planning-embed { + font-size: 0.93rem; +} + +.training-unit-sections-editor .combo-plan-bracket--planning-embed .combo-plan-bracket__station { + padding: 8px 9px; +} + +.training-unit-sections-editor .combo-plan-bracket--planning-embed .combo-plan-bracket__chip { + padding: 5px 8px; +} + +.training-unit-sections-editor .combo-plan-bracket--planning-embed .combo-plan-bracket__globals-title { + font-size: 0.72rem; +} + +.training-unit-sections-editor .combo-plan-bracket--planning-embed .combo-plan-bracket__head-main { + flex-wrap: wrap; +} + +.training-unit-sections-editor .combo-plan-bracket--planning-embed .combo-plan-bracket__kicker { + font-size: 0.62rem; +} + +.training-unit-sections-editor .combo-plan-bracket--planning-embed .combo-plan-bracket__archetype { + font-size: 0.88rem; } .tu-planning-mod-tag { diff --git a/frontend/src/components/CombinationPlanBracket.jsx b/frontend/src/components/CombinationPlanBracket.jsx index 28438ee..cffe8bd 100644 --- a/frontend/src/components/CombinationPlanBracket.jsx +++ b/frontend/src/components/CombinationPlanBracket.jsx @@ -41,6 +41,7 @@ export default function CombinationPlanBracket({ /** 'none' | 'link' (Router) | 'button' (z. B. ExercisePeekModal / PWA-sicher) */ candidateInteraction = 'none', onCandidatePeek, + className, }) { const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : '' const archLabel = arch ? combinationArchetypeLabel(arch) : null @@ -59,7 +60,7 @@ export default function CombinationPlanBracket({ const coachHint = arch ? archetypeCoachHint(arch) : '' return ( -
+
diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 995802d..7ac7580 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -3,7 +3,7 @@ import { GripVertical, Pencil } from 'lucide-react' import CombinationMethodProfileEditor from './CombinationMethodProfileEditor' import CombinationPlanBracket from './CombinationPlanBracket' import { comboPlanningProfileJsonForEditor, effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile' -import { combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes' +import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes' import { cloneJsonSerializablePlanningProfile, comboSlotsOutlineForProfileEditor, @@ -13,7 +13,6 @@ import { sectionPlannedMinutes, } from '../utils/trainingUnitSectionsForm' import api from '../utils/api' -import { effectiveStationTimingSummary, readSlotProfilesV1 } from '../utils/combinationMethodProfileUi' import { isCompactTagLegendMode } from '../config/planningModuleUx' import { useAuth } from '../context/AuthContext' @@ -74,60 +73,6 @@ function compactComboPlanningCaption(it) { return overridden ? 'Planung angepasst' : 'wie Katalog' } -/** Globale Eckdaten aus effective profile (optional unter Stationenliste). */ -function comboRoughGlobalTimingHint(profileObj, archetypeKey) { - if (!profileObj || typeof profileObj !== 'object' || Array.isArray(profileObj)) return null - const bits = [] - const rounds = profileObj.rounds - const ws = profileObj.work_seconds - const rb = profileObj.rest_between_rounds_sec - const hint = profileObj.hint_step_duration_sec - const globRest = profileObj.rest_between_sets_sec - if (rounds != null && rounds !== '') bits.push(`${rounds} Runden`) - if (ws != null && ws !== '') bits.push(`${ws}s Arbeit`) - if (rb != null && rb !== '') bits.push(`Pause ${rb}s`) - if (globRest != null && globRest !== '') bits.push(`Sets-Pause ${globRest}s`) - if (hint != null && hint !== '') bits.push(`Orientierung ~${hint}s`) - const arch = (archetypeKey || '').trim() - if (arch === 'time_domain_interval') { - const iw = profileObj.interval_work_sec - const ir = profileObj.interval_rest_sec - const ig = profileObj.interval_groups - if (iw != null && iw !== '') bits.push(`${iw}s Intervall`) - if (ir != null && ir !== '') bits.push(`${ir}s Erholung`) - if (ig != null && ig !== '') bits.push(`${ig} Gruppen`) - } - return bits.length ? bits.join(' · ') : null -} - -/** Pro Station eine kompakte Textzeile für die Planungsliste. */ -function comboPlanningStripBulletTexts(it) { - const slots = sortCombinationSlotsForDisplay(it.combination_slots || []) - if (!slots.length) return [] - const mp = effectiveComboMethodProfile(it.catalog_method_profile || {}, it.planning_method_profile) - const archRaw = String(it.catalog_method_archetype || '').trim() - const byIx = new Map(readSlotProfilesV1(mp).map((r) => [Number(r.slot_index), r])) - const titles = it.combo_member_title_by_id || {} - return slots.map((slot, idx) => { - const siRaw = slot.slot_index - const siParsed = - siRaw === '' || siRaw == null ? idx : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10) - const ix = Number.isFinite(siParsed) ? siParsed : idx - const stationLbl = ((slot.title || '').trim() || `Station ${idx + 1}`) - const candIds = (slot.candidate_exercise_ids || []) - .map((raw) => (typeof raw === 'number' ? raw : parseInt(String(raw), 10))) - .filter((n) => Number.isFinite(n)) - const namesJoined = - candIds.length === 0 - ? '(keine Übung)' - : candIds.map((id) => titles[String(id)] || `Übung ${id}`).join(' ↔ ') - const timing = effectiveStationTimingSummary(archRaw, mp, byIx.get(ix)) - let line = `${stationLbl}: ${namesJoined}` - if (timing) line += ` · ${timing}` - return line - }) -} - /** Stabile Farbzurodnung aus Modul-ID (nur Darstellung). */ function planningModulePalette(moduleId) { const id = normalizedPlanningModuleChainId(moduleId) @@ -703,7 +648,8 @@ export default function TrainingUnitSectionsEditor({
{(!hideHeading || headingAccessory) ? ( @@ -1017,10 +963,6 @@ export default function TrainingUnitSectionsEditor({ const stripArchRaw = isCombination && it.exercise_id ? String(it.catalog_method_archetype || '').trim() : '' - const stripArchLbl = - stripArchRaw && isCombination ? combinationArchetypeLabel(stripArchRaw) : null - const stripBullets = - isCombination && it.exercise_id ? comboPlanningStripBulletTexts(it) : [] const stripMpEff = isCombination && it.exercise_id ? effectiveComboMethodProfile( @@ -1028,17 +970,15 @@ export default function TrainingUnitSectionsEditor({ it.planning_method_profile, ) : null - const stripGlobalRough = - isCombination && it.exercise_id && stripMpEff - ? comboRoughGlobalTimingHint(stripMpEff, stripArchRaw) - : null return ( {!planningCompactLegend && renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
@@ -1215,76 +1155,48 @@ export default function TrainingUnitSectionsEditor({
{isCombination && it.exercise_id ? ( -
-
-
- Archetyp:  - - {stripArchLbl || stripArchRaw || '—'} - - - {compactComboPlanningCaption(it)} - -
- {stripGlobalRough ? ( -
- Block:  - {stripGlobalRough} -
- ) : null} - {stripBullets.length > 0 ? ( -
    - {stripBullets.map((line, bi) => ( -
  • - {line} -
  • - ))} -
- ) : ( -
- Stationen laden oder noch keine Kombi-Stationen im Katalog … -
- )} +
+
+
- + {(it.combination_slots || []).length > 0 ? ( +
+ onPeekExercise(Number(exId), null, undefined) + : undefined + } + /> +
+ ) : ( +
+
+ Stationen werden geladen oder die Kombination hat im Katalog noch keine Stationsliste … +
+
+ )}
) : null} diff --git a/frontend/src/pages/ExerciseFormPage.jsx b/frontend/src/pages/ExerciseFormPage.jsx index 975fa07..b82904a 100644 --- a/frontend/src/pages/ExerciseFormPage.jsx +++ b/frontend/src/pages/ExerciseFormPage.jsx @@ -2403,18 +2403,6 @@ function ExerciseFormPage() { } /> )} - setComboStationPickerIx(null)} - exerciseKindAny={['simple']} - multiSelect - enableQuickCreateDraft - onSelectExercises={(picked) => { - if (comboStationPickerIx === null) return - mergePickedExercisesIntoSlot(comboStationPickerIx, picked) - setComboStationPickerIx(null) - }} - /> {reportTarget && ( )} + setComboStationPickerIx(null)} + exerciseKindAny={['simple']} + multiSelect + enableQuickCreateDraft + onSelectExercises={(picked) => { + if (comboStationPickerIx === null) return + mergePickedExercisesIntoSlot(comboStationPickerIx, picked) + setComboStationPickerIx(null) + }} + /> +

KI-Ausbaustufe: Backend laut Spec{' '} POST /api/exercises/ai/suggest und{' '}