diff --git a/frontend/src/app.css b/frontend/src/app.css index 92b4f80..70ef2a0 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -3012,7 +3012,7 @@ a.analysis-split__nav-item { line-height: 1.2; } -/* Horizontaler Überblick: äußerer Scroll‑Container (Zuverlässigkeit in Flex/Grid‑Eltern) */ +/* Horizontaler Überblick: äußerer Scroll‑Container (Desktop: breite Session‑Karten) */ .framework-slots-board-outer { container-type: inline-size; width: 100%; @@ -3029,8 +3029,22 @@ a.analysis-split__nav-item { scrollbar-gutter: stable; } +.framework-slots-board-outer--mobile-single { + overflow-x: visible; + scrollbar-gutter: auto; + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + padding-bottom: 4px; +} + +.framework-slots-board-outer--desktop { + scrollbar-gutter: stable; +} + @media (max-width: 1023px) { - .framework-slots-board-outer { + .framework-slots-board-outer:not(.framework-slots-board-outer--mobile-single) { scrollbar-gutter: auto; } } @@ -3047,13 +3061,12 @@ a.analysis-split__nav-item { scroll-snap-type: x proximity; } -/* Kartenbreite an den Scroll-Container koppeln (100vw wäre oft breiter als .app-main → horizontales Wischen der ganzen Seite) */ -.framework-slots-board .framework-slot-card { - flex: 0 0 min(300px, calc(100vw - 72px)); - width: min(300px, calc(100vw - 72px)); - min-width: min(300px, calc(100vw - 72px)); - height: min(520px, 72vh); - max-height: min(520px, 72vh); +.framework-slots-board--desktop-wide .framework-slot-card { + flex: 0 0 min(760px, max(560px, calc(100cqw - 48px))); + width: min(760px, max(560px, calc(100cqw - 48px))); + min-width: min(760px, max(560px, calc(100cqw - 48px))); + height: auto; + max-height: none; display: flex; flex-direction: column; margin-bottom: 0; @@ -3063,10 +3076,87 @@ a.analysis-split__nav-item { scroll-snap-align: start; box-sizing: border-box; } -.framework-slots-board .framework-slot-card { - flex: 0 0 min(300px, calc(100cqw - 24px)); - width: min(300px, calc(100cqw - 24px)); - min-width: min(300px, calc(100cqw - 24px)); + +.framework-slots-board--desktop-wide .framework-slot-card__plan-editor { + flex: 1; + min-height: 240px; + max-height: min(78vh, 1200px); + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; +} + +/* Ein Slot = nutzbare Bildschirmbreite; Chips oben/unten wechseln die Session */ +.framework-slot-mobile-panel { + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.framework-slot-mobile-panel .framework-slot-card--mobile-single { + flex: none; + width: 100%; + max-width: 100%; + min-width: 0; + height: auto; + max-height: none; + overflow: visible; + scroll-snap-align: unset; +} + +.framework-slot-mobile-panel .framework-slot-card__plan-editor { + max-height: none; + overflow-x: hidden; +} + +.framework-slot-chips-bar { + display: flex; + flex-wrap: nowrap; + gap: 8px; + overflow-x: auto; + overflow-y: hidden; + padding: 8px 0 10px; + margin-bottom: 2px; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; +} + +.framework-slot-chips-bar--bottom { + margin-bottom: 0; + margin-top: 12px; + padding-top: 6px; + border-top: 1px solid var(--border); +} + +.framework-slot-chip { + flex: 0 0 auto; + appearance: none; + margin: 0; + cursor: pointer; + padding: 8px 16px; + border-radius: 999px; + border: 1px solid var(--border2); + background: var(--surface2); + color: var(--text2); + font-size: 0.86rem; + font-weight: 600; + line-height: 1.25; + max-width: 220px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.framework-slot-chip:hover { + border-color: var(--accent); + color: var(--accent-dark); +} + +.framework-slot-chip--active { + background: var(--accent); + border-color: var(--accent); + color: var(--accent-text, #fff); } .framework-slot-card__head { @@ -3295,11 +3385,6 @@ a.analysis-split__nav-item { } @media (min-width: 900px) { - .framework-slots-board .framework-slot-card { - flex-basis: 300px; - width: 300px; - min-width: 300px; - } .framework-slot-card__slot-actions { flex-direction: row; align-items: center; diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 727751f..5e82b06 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -17,6 +17,7 @@ export default function TrainingUnitSectionsEditor({ showExecutionExtras = false, heading = 'Abschnitte & Übungen', hideHeading = false, + wideExerciseGrid = false, }) { const ensure = (prev) => prev && prev.length ? prev : [defaultSection()] @@ -233,7 +234,9 @@ export default function TrainingUnitSectionsEditor({ key={`ex-${sIdx}-${iIdx}`} style={{ display: 'grid', - gridTemplateColumns: '28px minmax(0, 1fr) minmax(0, 64px) 36px', + gridTemplateColumns: wideExerciseGrid + ? '32px minmax(0, 1fr) minmax(96px, 220px) 44px' + : '28px minmax(0, 1fr) minmax(0, 64px) 36px', gap: '6px', alignItems: 'start', marginTop: '0.65rem', diff --git a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx index fa95a29..b9aa06b 100644 --- a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx +++ b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx @@ -25,6 +25,11 @@ function reorderArray(arr, from, to) { return next } +function slotChipLabel(slot, idx) { + const t = (slot?.title || '').trim() + return t || `Session ${idx + 1}` +} + function emptyGoal() { return { title: '', notes: '' } } @@ -187,6 +192,8 @@ export default function TrainingFrameworkProgramEditPage() { ? window.matchMedia(`(min-width: ${FRAMEWORK_DESKTOP_MIN_PX}px)`).matches : false ) + /** Schmale Ansicht: welcher Session-Slot gerade die volle Breite nutzt (Chip-Navigation) */ + const [mobileSlotIdx, setMobileSlotIdx] = useState(0) useEffect(() => { const mq = window.matchMedia(`(min-width: ${FRAMEWORK_DESKTOP_MIN_PX}px)`) @@ -233,6 +240,10 @@ export default function TrainingFrameworkProgramEditPage() { loadMeta() }, [loadMeta]) + useEffect(() => { + setMobileSlotIdx(0) + }, [idParam, isNew]) + useEffect(() => { if (isNew) { setForm(defaultForm()) @@ -307,13 +318,32 @@ export default function TrainingFrameworkProgramEditPage() { if (j < 0 || j >= prev.slots.length) return prev const sl = [...prev.slots] ;[sl[idx], sl[j]] = [sl[j], sl[idx]] + setMobileSlotIdx((mi) => { + if (mi === idx) return j + if (mi === j) return idx + return mi + }) return { ...prev, slots: sl } }) } - const addSlot = () => setForm((prev) => ({ ...prev, slots: [...prev.slots, emptySlot()] })) - const removeSlot = (idx) => + const addSlot = () => { + setForm((prev) => { + const slots = [...prev.slots, emptySlot()] + setMobileSlotIdx(slots.length - 1) + return { ...prev, slots } + }) + } + + const removeSlot = (idx) => { + const n = form.slots.length + setMobileSlotIdx((mi) => { + if (idx < mi) return Math.max(0, mi - 1) + if (idx === mi) return Math.min(mi, Math.max(0, n - 2)) + return mi + }) setForm((prev) => ({ ...prev, slots: prev.slots.filter((_, i) => i !== idx) })) + } const slotField = (sIdx, key, val) => { setForm((prev) => ({ @@ -440,6 +470,129 @@ export default function TrainingFrameworkProgramEditPage() { }) } + const slotChipButtons = (opts) => + form.slots.map((slot, si) => { + const isActive = si === mobileSlotIdx + const sel = opts?.tabSemantics + const baseClass = `framework-slot-chip${isActive ? ' framework-slot-chip--active' : ''}` + const attrs = sel + ? { role: 'tab', id: `fw-slot-chip-${si}`, 'aria-selected': isActive } + : { 'aria-pressed': isActive } + return ( + + ) + }) + + const renderFrameworkSlotCard = (si) => { + const slot = form.slots[si] + if (!slot) return null + return ( +