diff --git a/frontend/src/app.css b/frontend/src/app.css index 44d1318..9f93655 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5128,6 +5128,64 @@ a.analysis-split__nav-item { letter-spacing: 0.01em; } +/* Einfügen zwischen Ablaufzeilen (Übung / Modul / Anmerkung) */ +.tu-insert-slot { + display: flex; + align-items: center; + justify-content: center; + min-height: 11px; + margin: -1px 0 3px; + padding: 0 4px; +} + +.tu-insert-slot__btn { + appearance: none; + margin: 0; + cursor: pointer; + border: 1px dashed var(--border2); + background: color-mix(in srgb, var(--surface2) 90%, transparent); + color: var(--accent-dark); + font-size: 0.9rem; + font-weight: 700; + line-height: 1; + padding: 2px 9px; + border-radius: 999px; + opacity: 0.78; +} + +.tu-insert-slot__btn:hover, +.tu-insert-slot__btn:focus-visible { + opacity: 1; + outline: none; + border-color: var(--accent); + background: color-mix(in srgb, var(--accent-light) 40%, var(--surface2)); +} + +.tu-insert-chooser-actions { + display: flex; + flex-direction: column; + gap: 10px; +} + +.tu-insert-chooser-actions__full { + width: 100%; + justify-content: center; +} + +.tu-item-row--separator-note { + padding-top: 0.35rem; + padding-bottom: 0.35rem; +} + +.tu-item-row__separator-line { + width: 100%; + margin: 0.2rem 0 0; + min-height: 1px; + border: none; + border-top: 2px solid var(--border); + opacity: 0.92; +} + .tu-item-row { display: flex; flex-wrap: wrap; diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 27f790d..fee6313 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -10,6 +10,9 @@ import { const DND_TU_ITEM = 'application/x-shinkan-training-unit-item' const DND_TU_SECTION = 'application/x-shinkan-training-section-v1' +/** Optische Trennlinie: wird als normale Zwischen-Anmerkung gespeichert (Inhalt nur dieser Marker). */ +const SECTION_INSERT_SEPARATOR_BODY = '---' + function normalizedPlanningModuleChainId(raw) { if (raw == null || raw === '') return null const n = typeof raw === 'number' ? raw : Number(raw) @@ -48,6 +51,7 @@ export default function TrainingUnitSectionsEditor({ sections, onSectionsChange, onRequestExercisePick, + onRequestTrainingModulePick, onPeekExercise, showExecutionExtras = false, heading = 'Abschnitte & Übungen', @@ -58,6 +62,8 @@ export default function TrainingUnitSectionsEditor({ enableSectionDragReorder = true, slotIndex = null, onMoveSectionsAcrossSlots = null, + /** Dünnes „+“ zwischen Einträge: Popup für Typ (Übung, Modul, …) */ + betweenInsertMenus = true, }) { const ensure = (prev) => prev && prev.length ? prev : [defaultSection()] @@ -99,16 +105,32 @@ export default function TrainingUnitSectionsEditor({ }) } + const insertItemAt = useCallback( + (sIdx, beforeIx, row) => { + patch((prev) => + prev.map((s, i) => { + if (i !== sIdx) return s + const items = [...(s.items || [])] + const ix = Math.max( + 0, + Math.min(Number(beforeIx) || 0, items.length) + ) + items.splice(ix, 0, row) + return { ...s, items } + }) + ) + }, + [patch] + ) + const addItem = (sIdx, kind) => { patch((prev) => - prev.map((s, i) => - i !== sIdx - ? s - : { - ...s, - items: [...(s.items || []), kind === 'note' ? noteRow() : exerciseRow()], - } - ) + prev.map((s, i) => { + if (i !== sIdx) return s + const items = [...(s.items || [])] + items.push(kind === 'note' ? noteRow() : exerciseRow()) + return { ...s, items } + }) ) } @@ -149,6 +171,8 @@ export default function TrainingUnitSectionsEditor({ } const [textEdit, setTextEdit] = useState(null) + /** { sIdx: number, beforeIx: number } – Einfüge-Popup („+“ zwischen Zeilen) */ + const [insertChooser, setInsertChooser] = useState(null) const [draggingPos, setDraggingPos] = useState(null) const [dropTargetPos, setDropTargetPos] = useState(null) @@ -164,6 +188,20 @@ export default function TrainingUnitSectionsEditor({ return () => window.removeEventListener('keydown', onKey) }, [textEdit]) + useEffect(() => { + if (!insertChooser) return + const onKey = (e) => { + if (e.key === 'Escape') setInsertChooser(null) + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [insertChooser]) + + const closeInsertChooser = useCallback(() => setInsertChooser(null), []) + + const insertSlotKeyPrefix = + slotIndex !== null && slotIndex !== undefined ? `sl${slotIndex}-` : '' + const clearSectionDnD = () => setDropSectionBand(null) const onSectionDragStart = (e, sIdx) => { @@ -351,6 +389,29 @@ export default function TrainingUnitSectionsEditor({ setTextEdit(null) } + const renderBetweenInsertBand = (sIdx, beforeIx, itemCount) => { + const posLabel = + beforeIx === 0 + ? 'vor dem ersten Eintrag' + : beforeIx >= itemCount + ? 'am Ende des Abschnitts' + : `vor Eintrag ${beforeIx + 1}` + return ( +
- {noteHasText ? notePv : '—'} -
+ + {isSepLine ? 'Trennung' : 'Zwischen-Anmerkung'} + + {isSepLine ? ( + + ) : ( ++ {noteHasText ? notePv : '—'} +
+ )}+ Über die +-Zeilen zwischen den Einträgen fügst du an der gewünschten Stelle Inhalte ein. Reihenfolge + weiter per Ziehen oder den Pfeiltasten ändern. +
+ ) : ( ++ Die neue Zeile erscheint genau hier; Reihenfolge kannst du wie gewohnt per Ziehen oder Pfeilen + ändern. +
+