diff --git a/.claude/docs/technical/NAV_RETURN_CONTEXT_SPEC.md b/.claude/docs/technical/NAV_RETURN_CONTEXT_SPEC.md index b196944..878ec7c 100644 --- a/.claude/docs/technical/NAV_RETURN_CONTEXT_SPEC.md +++ b/.claude/docs/technical/NAV_RETURN_CONTEXT_SPEC.md @@ -1,7 +1,7 @@ # Navigation — Return-Kontext (Rücksprung) **Stand:** 2026-05-20 -**Status:** Spezifikation + schrittweise Umsetzung (Pilot) +**Status:** Spezifikation + Phase 1–2 umgesetzt **Ziel:** In der PWA (ohne Browser-Back) zuverlässig an den fachlichen Ausgangspunkt zurückkehren — inkl. sinnvollem Label und optional UI-State. --- @@ -52,6 +52,13 @@ Router-State-Schlüssel: **`appReturn`** | `exerciseList` | — | `/exercises` (Filter/Auswahl via sessionStorage) | | `planningHub` | `buildPlanningHubReturnState(...)` | `planningHubPathFromReturnState(payload)` | | `trainingModulesList` | — | `/planning/training-modules` | +| `planTemplatesList` | — | `/planning/plan-templates` | +| `frameworkProgramsList` | — | `/planning/framework-programs` | +| `settings` | — | `/settings` | +| `dashboard` | — | `/` | +| `mediaLibrary` | — | `/media` | +| `trainingRun` | `{ unitId }` | `/planning/run/:unitId` | +| `currentLocation` | — | aktuelle Route (z. B. Einheiten-Editor) | | (frei) | — | `path` direkt gesetzt | ### Legacy-Kompatibilität @@ -76,14 +83,23 @@ Zentrale Datei: `frontend/src/utils/navReturnContext.js` | `navigateWithAppReturn(navigate, to, returnContext, options?)` | Navigation mit gesetztem `appReturn` | | `preserveAppReturnOnNavigate(navigate, location, to, options?)` | Weiterleiten, bestehenden Kontext behalten (z. B. nach `replace`) | -UI-Komponente: **`PageReturnLink`** — einheitlicher Zurück-Link oben auf Editor-/Detailseiten. +UI-Komponente: **`PageReturnButton`** — app-typischer Zurück-Schalter (Button mit Pfeil, kein Router-Link). +Links **zum** Ziel: **`NavStateLink`** mit `returnContext` der Quellseite. + +### Editor-Aktionen + +Auf Vollseiten-Editoren mit **`PageFormEditorChrome`** oder **`FormActionBar`** (`placement="bottom"`): + +- **Abbrechen** → `goBack()` / `goNavReturn(...)` (Einsprungspunkt, nicht feste Route) +- **Speichern & Schließen** → nach erfolgreichem Save ebenfalls `goBack()` +- Sticky Action Bar unten nutzen, wo vorhanden --- ## Regeln für Entwickler 1. **Jede Navigation** von Kontext A zu Editor B, wo der Nutzer „weitermachen“ soll, setzt `appReturn` (oder nutzt `navigateWithAppReturn`). -2. **Zielseite** zeigt `PageReturnLink` mit sinnvollem **Default-Fallback** (Bibliothek/Hub). +2. **Zielseite** zeigt `PageReturnButton` mit sinnvollem **Default-Fallback** (Bibliothek/Hub). 3. **Nach Create + `replace: true`:** Return-Kontext mit `preserveAppReturnOnNavigate` erhalten. 4. **Modals:** Schließen reicht; Redirect nach Speichern = Seiten-Navigation → Return setzen. 5. **Kein Return-Kontext** in `location.state` für interne Bibliothek → Detail → Bearbeiten, wenn Herkunft = offensichtliche Elternliste (Default-Fallback genügt). @@ -91,20 +107,30 @@ UI-Komponente: **`PageReturnLink`** — einheitlicher Zurück-Link oben auf Edit --- -## Pilot-Umsetzung (Phase 1) +## Umsetzungsstand + +### Phase 1 (Pilot) - [x] Spec + Utility + Tests -- [x] `PageReturnLink` -- [x] Übungsliste → Modul speichern → Modul-Editor (dynamischer Zurück-Link) +- [x] `PageReturnButton` (ersetzt Link-Variante) +- [x] Übungsliste → Modul speichern → Modul-Editor - [x] Planung: `SaveExercisesAsModuleModal` leitet Return-Kontext weiter - [x] `TrainingUnitEditPage`: `goBack` über `goNavReturn` (Legacy-bridge) -## Folge-Phasen (noch offen) +### Phase 2 (Flows verbinden) -- Weitere Editoren (Übung, Vorlage, Rahmenprogramm) -- Optional: globaler Zurück-Button in App-Chrome (Mobile) +- [x] Listen → Editoren: Übungen, Module, Vorlagen, Rahmenprogramme +- [x] Dashboard → Übung bearbeiten / Trainingsablauf / Einheit bearbeiten +- [x] Einstellungen-Unterseiten (Rechtliches, Systeminfo) +- [x] Trainingsablauf + Coach-Modus (`trainingRun`, Planungs-Fallback) +- [x] Medienbibliothek → verknüpfte Übungen/Einheiten +- [x] `ExercisePeekModal` → Vollseite mit Return +- [x] Editoren: Abbrechen + Speichern & Schließen → Einsprungspunkt + +### Optional (später) + +- Globaler Zurück-Button in App-Chrome (Mobile) - Nach Speichern: explizite Aktion „Zurück zum Ausgang“ im Toast -- `ExercisePeekModal` → Vollseite mit Return --- diff --git a/frontend/src/app.css b/frontend/src/app.css index 48bb744..05f59c5 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1466,6 +1466,21 @@ html.modal-scroll-locked .app-main { font-weight: 600; text-decoration: none; } +.page-return-btn { + display: inline-flex; + align-items: center; + gap: 6px; + margin-bottom: 0.75rem; + max-width: 100%; +} +.page-return-btn__icon { + flex-shrink: 0; +} +.page-return-btn span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .page-return-link:hover { text-decoration: underline; } diff --git a/frontend/src/components/ExercisePeekModal.jsx b/frontend/src/components/ExercisePeekModal.jsx index 9fae195..0afc490 100644 --- a/frontend/src/components/ExercisePeekModal.jsx +++ b/frontend/src/components/ExercisePeekModal.jsx @@ -5,6 +5,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' +import NavStateLink from './NavStateLink' import ExerciseRichTextBlock from './ExerciseRichTextBlock' import CombinationPlanBracket from './CombinationPlanBracket' import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile' @@ -36,6 +37,8 @@ export default function ExercisePeekModal({ titleFallback, /** Nur Planung: effektives method_profile aus Zeilen-Katalog + Planungs-Override */ peekExtras, + /** Rücksprung-Kontext für „Vollständige Übungsseite“ */ + returnContext, }) { const [loading, setLoading] = useState(false) const [err, setErr] = useState(null) @@ -255,13 +258,24 @@ export default function ExercisePeekModal({ {top?.exerciseId != null ? (
diff --git a/frontend/src/components/exercises/ExerciseListCard.jsx b/frontend/src/components/exercises/ExerciseListCard.jsx index 84e7bc8..a0d1aee 100644 --- a/frontend/src/components/exercises/ExerciseListCard.jsx +++ b/frontend/src/components/exercises/ExerciseListCard.jsx @@ -1,5 +1,10 @@ -import React from 'react' -import { Link, useNavigate } from 'react-router-dom' +import React, { useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import NavStateLink from '../NavStateLink' +import { + buildExercisesListReturnContext, + navigateWithAppReturn, +} from '../../utils/navReturnContext' import { Eye, Pencil, @@ -141,12 +146,14 @@ export default function ExerciseListCard({ selectionPinned = false, }) { const navigate = useNavigate() + const listReturn = useMemo(() => buildExercisesListReturnContext(), []) const focusNames = exerciseFocusNames(exercise) const styleNames = coerceApiNameList(exercise.style_direction_names) const typeNames = coerceApiNameList(exercise.training_type_names) const titleText = (exercise.title || 'Übung').replace(/"/g, '') - const openExercisePage = () => navigate(`/exercises/${exercise.id}`) + const openExercisePage = () => + navigateWithAppReturn(navigate, `/exercises/${exercise.id}`, listReturn) const handleBodyClick = (e) => { if (e.target.closest('a, button, input, textarea, select, label, [role="button"]')) return @@ -186,9 +193,13 @@ export default function ExerciseListCard({ aria-label={`„${titleText}“ öffnen`} >
{msg}
- +