feat: enhance training framework UI and responsiveness
- Updated app.css to improve layout and responsiveness for mobile and desktop views, including new styles for session slots and chips. - Modified TrainingUnitSectionsEditor to support a wider exercise grid for better usability. - Enhanced TrainingFrameworkProgramEditPage with improved slot management and mobile navigation features. - Introduced dynamic slot chip labels for better user interaction and clarity.
This commit is contained in:
parent
c4fbabd8f6
commit
69adf1fde0
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<button key={si} type="button" {...attrs} className={baseClass} onClick={() => setMobileSlotIdx(si)} title={slotChipLabel(slot, si)}>
|
||||
{slotChipLabel(slot, si)}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
const renderFrameworkSlotCard = (si) => {
|
||||
const slot = form.slots[si]
|
||||
if (!slot) return null
|
||||
return (
|
||||
<div
|
||||
key={si}
|
||||
className={`card framework-slot-card${!desktopLayout ? ' framework-slot-card--mobile-single' : ''}`}
|
||||
onDragOver={desktopLayout ? onSlotColumnDragOver : undefined}
|
||||
onDrop={desktopLayout ? (e) => onSlotColumnDrop(e, si) : undefined}
|
||||
>
|
||||
<div className="framework-slot-card__head">
|
||||
{desktopLayout ? (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="framework-slot-card__drag-handle"
|
||||
draggable
|
||||
onDragStart={(e) => onSlotDragStart(e, si)}
|
||||
aria-label="Slot ziehen: Reihenfolge ändern"
|
||||
title="Slot ziehen (Reihenfolge)"
|
||||
>
|
||||
⋮⋮
|
||||
</span>
|
||||
) : null}
|
||||
<div className="framework-slot-card__head-main">
|
||||
<span className="framework-slot-card__slot-label">Session {si + 1}</span>
|
||||
<input
|
||||
className="form-input framework-slot-card__title-input"
|
||||
value={slot.title}
|
||||
onChange={(e) => slotField(si, 'title', e.target.value)}
|
||||
placeholder={`z. B. Woche ${si + 1}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="framework-slot-card__slot-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => moveSlot(si, -1)}
|
||||
aria-label="Slot nach links / oben"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => moveSlot(si, 1)}
|
||||
aria-label="Slot nach rechts / unten"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => removeSlot(si)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details className="framework-slot-details">
|
||||
<summary className="framework-slot-details__summary">Notizen (Session)</summary>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Notizen</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={slot.notes}
|
||||
onChange={(e) => slotField(si, 'notes', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="framework-slot-card__plan-editor" style={{ marginTop: '0.65rem', minHeight: '120px' }}>
|
||||
<TrainingUnitSectionsEditor
|
||||
heading={`Ablauf · Session ${si + 1}`}
|
||||
sections={slot.sections}
|
||||
showExecutionExtras={false}
|
||||
wideExerciseGrid
|
||||
onSectionsChange={(updater) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
slots: prev.slots.map((sl, ii) =>
|
||||
ii !== si
|
||||
? sl
|
||||
: {
|
||||
...sl,
|
||||
sections: updater(
|
||||
sl.sections && sl.sections.length ? sl.sections : [defaultSection('Ablauf')]
|
||||
),
|
||||
}
|
||||
),
|
||||
}))
|
||||
}}
|
||||
onRequestExercisePick={({ sectionIndex, itemIndex }) =>
|
||||
setSectionPickerCtx({
|
||||
slotIdx: si,
|
||||
sectionIndex,
|
||||
itemIndex,
|
||||
})
|
||||
}
|
||||
onPeekExercise={(id) => setPeekId(id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="app-page" style={{ padding: '2rem 0', textAlign: 'center' }}>
|
||||
|
|
@ -840,114 +993,49 @@ export default function TrainingFrameworkProgramEditPage() {
|
|||
className="framework-slots-hint"
|
||||
style={{ fontSize: '0.8rem', color: 'var(--text3)', marginBottom: '10px', lineHeight: 1.45 }}
|
||||
>
|
||||
Reihenfolge der Spalten per Griff (⋮⋮) ziehen; Inhalt eines Slots wie in der Planung bearbeiten.
|
||||
{desktopLayout ? (
|
||||
<>
|
||||
Reihenfolge der Spalten per Griff (⋮⋮) ziehen oder mit ↑ / ↓ verschieben; Inhalt eines Slots wie
|
||||
in der Planung bearbeiten (Abschnitte, Übungen, Anmerkungen).
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Sessions oben oder unten per Chips wählen — ein Slot nutzt die volle Breite. Reihenfolge mit ↑ / ↓;
|
||||
bearbeiten wie in der Planung.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="framework-slots-board-outer">
|
||||
<div className="framework-slots-board">
|
||||
{form.slots.map((slot, si) => (
|
||||
{form.slots.length > 0 ? (
|
||||
!desktopLayout ? (
|
||||
<div className="framework-slots-board-outer framework-slots-board-outer--mobile-single">
|
||||
<div
|
||||
key={si}
|
||||
className="card framework-slot-card"
|
||||
onDragOver={onSlotColumnDragOver}
|
||||
onDrop={(e) => onSlotColumnDrop(e, si)}
|
||||
className="framework-slot-chips-bar framework-slot-chips-bar--top"
|
||||
role="tablist"
|
||||
aria-label="Sessions"
|
||||
>
|
||||
<div className="framework-slot-card__head">
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="framework-slot-card__drag-handle"
|
||||
draggable
|
||||
onDragStart={(e) => onSlotDragStart(e, si)}
|
||||
aria-label="Slot ziehen: Reihenfolge ändern"
|
||||
title="Slot ziehen (Reihenfolge)"
|
||||
>
|
||||
⋮⋮
|
||||
</span>
|
||||
<div className="framework-slot-card__head-main">
|
||||
<span className="framework-slot-card__slot-label">Session {si + 1}</span>
|
||||
<input
|
||||
className="form-input framework-slot-card__title-input"
|
||||
value={slot.title}
|
||||
onChange={(e) => slotField(si, 'title', e.target.value)}
|
||||
placeholder={`z. B. Woche ${si + 1}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="framework-slot-card__slot-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => moveSlot(si, -1)}
|
||||
aria-label="Slot nach links / oben"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => moveSlot(si, 1)}
|
||||
aria-label="Slot nach rechts / unten"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => removeSlot(si)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details className="framework-slot-details">
|
||||
<summary className="framework-slot-details__summary">Notizen (Session)</summary>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Notizen</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={slot.notes}
|
||||
onChange={(e) => slotField(si, 'notes', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="framework-slot-card__plan-editor" style={{ marginTop: '0.65rem', minHeight: '120px' }}>
|
||||
<TrainingUnitSectionsEditor
|
||||
heading={`Ablauf · Session ${si + 1}`}
|
||||
sections={slot.sections}
|
||||
showExecutionExtras={false}
|
||||
onSectionsChange={(updater) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
slots: prev.slots.map((sl, ii) =>
|
||||
ii !== si
|
||||
? sl
|
||||
: {
|
||||
...sl,
|
||||
sections: updater(
|
||||
sl.sections && sl.sections.length ? sl.sections : [defaultSection('Ablauf')]
|
||||
),
|
||||
}
|
||||
),
|
||||
}))
|
||||
}}
|
||||
onRequestExercisePick={({ sectionIndex, itemIndex }) =>
|
||||
setSectionPickerCtx({
|
||||
slotIdx: si,
|
||||
sectionIndex,
|
||||
itemIndex,
|
||||
})
|
||||
}
|
||||
onPeekExercise={(id) => setPeekId(id)}
|
||||
/>
|
||||
</div>
|
||||
{slotChipButtons({ tabSemantics: true })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="framework-slot-mobile-panel" tabIndex={-1}>
|
||||
{renderFrameworkSlotCard(mobileSlotIdx)}
|
||||
</div>
|
||||
<div
|
||||
className="framework-slot-chips-bar framework-slot-chips-bar--bottom"
|
||||
role="group"
|
||||
aria-label="Sessions (Anwahl unten)"
|
||||
>
|
||||
{slotChipButtons({ tabSemantics: false })}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="framework-slots-board-outer framework-slots-board-outer--desktop">
|
||||
<div className="framework-slots-board framework-slots-board--desktop-wide">
|
||||
{form.slots.map((_, si) => renderFrameworkSlotCard(si))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user