From 8f32a6df29eb263b2415d44d008afc5fb86bfa76 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 5 May 2026 12:52:51 +0200 Subject: [PATCH] feat: enhance TrainingFrameworkProgramEditPage with goal management and UI improvements - Introduced new goal chip components for better visual representation and interaction. - Added hover tooltips for goal chips to display additional information. - Implemented state management for editing goals and goal menu visibility. - Updated styles in app.css to support new goal chip design and improve layout consistency. - Incremented version of TrainingFrameworkProgramEditPage to 1.4.0 to reflect the latest enhancements. --- frontend/src/app.css | 163 ++++++- .../TrainingFrameworkProgramEditPage.jsx | 439 ++++++++++++------ frontend/src/version.js | 2 +- 3 files changed, 448 insertions(+), 156 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 79648dc..52c525e 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -2856,6 +2856,130 @@ a.analysis-split__nav-item { border-left: 3px solid var(--accent); } +.framework-goal-chips__hint { + font-size: 0.78rem; + color: var(--text3); + margin: 0 0 10px; + line-height: 1.45; +} + +.framework-goal-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.framework-popmenu-anchor { + position: relative; + display: inline-flex; + align-items: stretch; + vertical-align: middle; + max-width: 100%; +} + +.framework-goal-chip-wrap { + border-radius: 999px; + background: var(--surface2); + border: 1px solid var(--border2); +} + +.framework-goal-chip { + border: none; + background: transparent; + font: inherit; + padding: 6px 10px 6px 12px; + border-radius: 999px 0 0 999px; + cursor: pointer; + max-width: min(240px, 100%); + text-align: left; +} + +.framework-goal-chip--active { + background: var(--accent-light); +} + +.framework-goal-chip__text { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.88rem; + font-weight: 600; + color: var(--text1); +} + +.framework-goal-chip__kebab { + border: none; + background: transparent; + padding: 0 8px; + cursor: pointer; + color: var(--text3); + font-size: 1.05rem; + line-height: 1; + border-left: 1px solid var(--border); + border-radius: 0 999px 999px 0; +} + +.framework-goal-chip__kebab:hover, +.framework-goal-chip:hover { + filter: brightness(0.97); +} + +.framework-goal-editor { + margin-top: 12px; + padding: 12px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--surface2); +} + +.framework-popmenu { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 30; + margin: 0; + padding: 4px 0; + list-style: none; + min-width: 200px; + max-width: min(300px, calc(100vw - 32px)); + background: var(--surface); + border: 1px solid var(--border2); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +.framework-popmenu--align-end { + left: auto; + right: 0; +} + +.framework-popmenu li { + margin: 0; +} + +.framework-popmenu__item { + display: block; + width: 100%; + text-align: left; + border: none; + background: transparent; + font-family: var(--font); + font-size: 13px; + padding: 10px 14px; + cursor: pointer; + color: var(--text1); +} + +.framework-popmenu__item:hover { + background: var(--surface2); +} + +.framework-popmenu__item--danger { + color: var(--danger); +} + .framework-ctrl.framework-ctrl--xs { padding: 2px 8px; font-size: 11px; @@ -3090,16 +3214,45 @@ a.analysis-split__nav-item { color: var(--text3); } -.framework-ex-row__toolbar { +.framework-ex-row__row2 { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; - gap: 6px; + gap: 8px; + width: 100%; + min-width: 0; +} + +.framework-ex-row__variant-spacer { + flex: 1; + min-width: 0; +} + +.framework-ex-row__menu-anchor { + flex: 0 0 auto; +} + +.framework-ex-row__kebab { + width: 34px; + height: 34px; + padding: 0; + border-radius: 8px; + border: 1px solid var(--border2); + background: var(--surface); + cursor: pointer; + font-size: 1.1rem; + line-height: 1; + color: var(--text2); +} + +.framework-ex-row__kebab:hover { + border-color: var(--accent); + color: var(--accent-dark); } .framework-ex-row__variant-select { - flex: 1 1 140px; - min-width: 120px; + flex: 1 1 auto; + min-width: 100px; max-width: 100%; padding: 6px 8px; font-size: 13px; diff --git a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx index b4d1b3d..9787937 100644 --- a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx +++ b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx @@ -31,6 +31,15 @@ function emptySlot() { return { title: '', notes: '', training_unit_id: '', exercises: [] } } +/** Native-Tooltip für Ziel-Chips (Hover); kurz halten für OS-Tooltip-Limits */ +function goalHoverText(g) { + const t = (g.title || '').trim() || 'Ohne Titel' + const n = (g.notes || '').trim() + if (!n) return t + const combined = `${t} — ${n}` + return combined.length > 280 ? `${combined.slice(0, 277)}…` : combined +} + function defaultForm() { return { title: '', @@ -190,6 +199,9 @@ export default function TrainingFrameworkProgramEditPage() { const [units, setUnits] = useState([]) const [pickerSlotIdx, setPickerSlotIdx] = useState(null) const [peekId, setPeekId] = useState(null) + const [editingGoalIdx, setEditingGoalIdx] = useState(null) + const [goalMenuGi, setGoalMenuGi] = useState(null) + const [exerciseMenu, setExerciseMenu] = useState(null) /** Nur schmal: Stammdaten | Plan (Ziele übereinander, darunter Slots) — Desktop: alles sichtbar */ const [frameworkTab, setFrameworkTab] = useState('meta') const [desktopLayout, setDesktopLayout] = useState( @@ -206,6 +218,17 @@ export default function TrainingFrameworkProgramEditPage() { return () => mq.removeEventListener('change', apply) }, []) + useEffect(() => { + const onPointerDown = (e) => { + const t = e.target + if (t.closest?.('.framework-popmenu-anchor')) return + setExerciseMenu(null) + setGoalMenuGi(null) + } + document.addEventListener('pointerdown', onPointerDown, true) + return () => document.removeEventListener('pointerdown', onPointerDown, true) + }, []) + const loadMeta = useCallback(async () => { try { const [gr, cl] = await Promise.all([ @@ -295,6 +318,8 @@ export default function TrainingFrameworkProgramEditPage() { } const moveGoal = (idx, dir) => { + setEditingGoalIdx(null) + setGoalMenuGi(null) setForm((prev) => { const j = idx + dir if (j < 0 || j >= prev.goals.length) return prev @@ -304,12 +329,25 @@ export default function TrainingFrameworkProgramEditPage() { }) } - const addGoal = () => setForm((prev) => ({ ...prev, goals: [...prev.goals, emptyGoal()] })) - const removeGoal = (idx) => + const addGoal = () => { + let newIdx = 0 + setForm((prev) => { + const goals = [...prev.goals, emptyGoal()] + newIdx = goals.length - 1 + return { ...prev, goals } + }) + setEditingGoalIdx(newIdx) + setGoalMenuGi(null) + } + + const removeGoal = (idx) => { setForm((prev) => { const g = prev.goals.filter((_, i) => i !== idx) return { ...prev, goals: g.length ? g : [emptyGoal()] } }) + setEditingGoalIdx(null) + setGoalMenuGi(null) + } const moveSlot = (idx, dir) => { setForm((prev) => { @@ -487,33 +525,29 @@ export default function TrainingFrameworkProgramEditPage() { } const onExerciseDragStart = (e, fromS, fromE) => { - if (!desktopLayout) return e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData(DND_FW_EX, JSON.stringify({ fromS, fromE })) + setExerciseMenu(null) } const onExerciseDragOver = (e) => { - if (!desktopLayout) return e.preventDefault() e.dataTransfer.dropEffect = 'move' } const onSlotDragStart = (e, slotIdx) => { - if (!desktopLayout) return e.stopPropagation() e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData(DND_FW_SLOT, JSON.stringify({ slotIdx })) } const onSlotColumnDragOver = (e) => { - if (!desktopLayout) return e.preventDefault() e.dataTransfer.dropEffect = 'move' } const onSlotColumnDrop = (e, targetSi) => { e.preventDefault() - if (!desktopLayout) return const slotRaw = e.dataTransfer.getData(DND_FW_SLOT) if (slotRaw) { const { slotIdx } = JSON.parse(slotRaw) @@ -740,7 +774,7 @@ export default function TrainingFrameworkProgramEditPage() { gap: '8px', justifyContent: 'space-between', alignItems: 'center', - marginBottom: '0.75rem', + marginBottom: '0.6rem', }} >

@@ -750,72 +784,138 @@ export default function TrainingFrameworkProgramEditPage() { + Ziel - {(form.goals || []).map((g, gi) => ( -
-
+

+ Ziele als Tags — Tippen zum Bearbeiten; am Desktop zeigt der Titel-Tooltip die Notizen mit. Am + Handy: oder Bearbeiten. +

+
+ {(form.goals || []).map((g, gi) => ( +
- + {goalMenuGi === gi ? ( +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ ) : null}
+ ))} +
+ {editingGoalIdx != null && form.goals[editingGoalIdx] != null ? ( +
setForm((prev) => ({ ...prev, - goals: prev.goals.map((x, i) => (i === gi ? { ...x, title: e.target.value } : x)), + goals: prev.goals.map((x, i) => + i === editingGoalIdx ? { ...x, title: e.target.value } : x + ), })) } />
-
+