diff --git a/frontend/src/app.css b/frontend/src/app.css index 72415e5..ff76e8b 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5402,6 +5402,103 @@ a.analysis-split__nav-item { inset 10px 0 0 color-mix(in srgb, var(--accent) 14%, transparent); } +/* Kompakt: Modulfarbe über CSS-Variable --tu-mod-border (pro Zeile gesetzt). */ +.training-unit-sections-editor .tu-item-row--from-module-soft.tu-item-row--exercise, +.training-unit-sections-editor .tu-item-row--from-module-soft.tu-item-row--note:not(.tu-item-row--separator-note) { + border-left: 4px solid var(--tu-mod-border, var(--accent)); + padding-left: 0.62rem; + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.52) inset, + 0 2px 12px rgba(15, 23, 42, 0.05); +} + +.tu-planning-mod-tag { + display: inline-flex; + align-items: center; + gap: 5px; + flex: 0 1 auto; + max-width: 100%; + margin: 0; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--border2) 70%, transparent); + font-size: 0.72rem; + font-weight: 700; + line-height: 1.35; + color: var(--text1); +} + +.tu-planning-mod-tag__dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.tu-planning-mod-tag__text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tu-section-mod-legend { + margin-top: 0.75rem; + padding: 0.5rem 0.62rem; + border-radius: 11px; + border: 1px solid color-mix(in srgb, var(--border2) 80%, transparent); + background: color-mix(in srgb, var(--surface) 92%, transparent); +} + +.tu-section-mod-legend__caption { + font-size: 0.65rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text3); + margin-bottom: 0.42rem; +} + +.tu-section-mod-legend__list { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 0.42rem 0.72rem; +} + +.tu-section-mod-legend__item { + display: inline-flex; + align-items: flex-start; + gap: 8px; + flex: 1 1 200px; + min-width: 0; +} + +.tu-section-mod-legend__swatch { + width: 11px; + height: 11px; + border-radius: 3px; + flex-shrink: 0; + margin-top: 0.26rem; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06); +} + +.tu-section-mod-legend__title { + display: block; + font-size: 0.82rem; + font-weight: 700; + line-height: 1.35; + color: var(--text1); +} + +.tu-section-mod-legend__meta { + display: block; + font-size: 0.72rem; + line-height: 1.42; + color: var(--text3); +} + .tu-item-row { display: flex; flex-wrap: wrap; diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index d12002d..281d877 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -6,6 +6,7 @@ import { noteRow, sectionPlannedMinutes, } from '../utils/trainingUnitSectionsForm' +import { PLANNING_MODULE_UX_MODE } from '../config/planningModuleUx' const DND_TU_ITEM = 'application/x-shinkan-training-unit-item' const DND_TU_SECTION = 'application/x-shinkan-training-section-v1' @@ -55,6 +56,61 @@ function gatherPlanningModuleOutline(items, startIdx, moduleId) { const MODULE_OUTLINE_PREVIEW_MAX = 8 +const PLANNING_USE_COMPACT_LEGEND = PLANNING_MODULE_UX_MODE === 'compact_tag_legend' + +/** Stabile Farbzurodnung aus Modul-ID (nur Darstellung). */ +function planningModulePalette(moduleId) { + const id = normalizedPlanningModuleChainId(moduleId) + const n = id != null && id >= 1 ? Math.floor(Number(id)) : 1 + const golden = ((n >>> 0) * 2654435761 + n * 73856093) >>> 0 + const h = golden % 360 + const border = `hsl(${h} 52% 36%)` + const soft = `hsl(${h} 42% 94%)` + return { border, soft, hue: h } +} + +function PlanningModuleRowTag({ moduleId, title }) { + const p = planningModulePalette(moduleId) + const lbl = truncatePreview(title || `Modul #${moduleId}`, 34).trim() + const fullTitle = ((title || '').trim() || `Modul #${moduleId}`).trim() + return ( + + + Aus Modul: {lbl} + + ) +} + +/** Eindeutige Module im Abschnitt mit Zählerständen für die Legende. */ +function sectionModuleLegendModel(items) { + const map = new Map() + for (const row of items || []) { + const id = normalizedPlanningModuleChainId(row.source_training_module_id) + if (id == null) continue + if (!map.has(id)) { + map.set(id, { + id, + title: (((row.source_module_title || '').trim() || '') || `Modul #${id}`).trim(), + exercises: 0, + notes: 0, + }) + } + const agg = map.get(id) + if ((row.item_type || '') === 'note') { + const bod = ((row.note_body || '').trim() || '').trim() + if (bod === SECTION_INSERT_SEPARATOR_BODY) continue + agg.notes += 1 + } else { + agg.exercises += 1 + } + } + return [...map.values()].sort((a, b) => a.id - b.id) +} + function renderModulePlanningHead(modBandTitle, modOutline, showModuleBand) { if (!showModuleBand || !modOutline) return null return ( @@ -515,6 +571,7 @@ export default function TrainingUnitSectionsEditor({ {list.map((sec, sIdx) => { const planMin = sectionPlannedMinutes(sec) const itemCount = sec.items?.length ?? 0 + const moduleLegend = PLANNING_USE_COMPACT_LEGEND ? sectionModuleLegendModel(sec.items) : [] const bandActiveBefore = (bx) => enableSectionDragReorder && dropSectionBand && @@ -656,8 +713,21 @@ export default function TrainingUnitSectionsEditor({ (curMn != null ? `Modul #${curMn}` : '') const modOutline = - showModuleBand && curMn != null ? gatherPlanningModuleOutline(sec.items, iIdx, curMn) : null - const fromModClass = curMn != null ? ' tu-item-row--from-module' : '' + !PLANNING_USE_COMPACT_LEGEND && + showModuleBand && + curMn != null + ? gatherPlanningModuleOutline(sec.items, iIdx, curMn) + : null + const fromModClass = + curMn != null + ? PLANNING_USE_COMPACT_LEGEND + ? ' tu-item-row--from-module-soft' + : ' tu-item-row--from-module' + : '' + const modBorderVarStyle = + PLANNING_USE_COMPACT_LEGEND && curMn != null + ? { '--tu-mod-border': planningModulePalette(curMn).border } + : undefined if (it.item_type === 'note') { const isSepLine = (it.note_body || '').trim() === SECTION_INSERT_SEPARATOR_BODY @@ -665,7 +735,8 @@ export default function TrainingUnitSectionsEditor({ const noteHasText = Boolean((it.note_body || '').trim()) && !isSepLine return ( - {renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)} + {!PLANNING_USE_COMPACT_LEGEND && + renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
{enableItemDragReorder ? (
+ {!isSepLine && PLANNING_USE_COMPACT_LEGEND && curMn ? ( + + ) : null} {isSepLine ? 'Trennung' : 'Zwischen-Anmerkung'} @@ -768,8 +843,13 @@ export default function TrainingUnitSectionsEditor({ return ( - {renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)} -
+ {!PLANNING_USE_COMPACT_LEGEND && + renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)} +
{enableItemDragReorder ? ( Keine Übung gewählt )} + {PLANNING_USE_COMPACT_LEGEND && curMn ? ( + + ) : null}
+ + {moduleLegend.length ? ( +
+
Übernommene Module im Abschnitt
+
    + {moduleLegend.map((e) => { + const pal = planningModulePalette(e.id) + return ( +
  • + + + + {(e.title || '').trim() || `Modul #${e.id}`} + + + ID {e.id} · {e.exercises}{' '} + {e.exercises === 1 ? 'Übung' : 'Übungen'} + {e.notes > 0 ? ( + <> + {' '} + · {e.notes} {e.notes === 1 ? 'Zwischen-Hinweis' : 'Zwischen-Hinweise'} + + ) : null} + + +
  • + ) + })} +
+
+ ) : null}
) diff --git a/frontend/src/config/planningModuleUx.js b/frontend/src/config/planningModuleUx.js new file mode 100644 index 0000000..22dad26 --- /dev/null +++ b/frontend/src/config/planningModuleUx.js @@ -0,0 +1,11 @@ +/** + * Darstellung „Herkunft Trainingsmodul“ in Abschnitten (Planungs-Editor). + * + * - compact_tag_legend (Standard): wenig Höhe — farbige Leiste am Eintrag, + * kleiner Modul-Tag in der Zeile, Legende pro Abschnitt unten (Farbe ⇄ Modul). + * - full_outline_headers: früheres Verhalten mit großem Kopf-Bereich inkl. + * Auflistung der Übungen (viel Platz, maximale Orientierung ohne Scroll). + * + * Zum Zurückschalten: Wert hier auf `'full_outline_headers'` setzen oder Datei reverten. + */ +export const PLANNING_MODULE_UX_MODE = 'compact_tag_legend'