feat(training-units): add compact legend and module styling in Training Unit Sections Editor
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 56s
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 56s
- Introduced new CSS styles for compact module display, enhancing the visual structure of training unit sections. - Implemented functionality to conditionally render module tags and borders based on the selected UX mode. - Enhanced the section module legend model to aggregate and display module information effectively. - Improved the rendering logic to support both compact and standard views, ensuring a flexible user experience. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e96951728d
commit
05042ee9ec
|
|
@ -5402,6 +5402,103 @@ a.analysis-split__nav-item {
|
||||||
inset 10px 0 0 color-mix(in srgb, var(--accent) 14%, transparent);
|
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 {
|
.tu-item-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
noteRow,
|
noteRow,
|
||||||
sectionPlannedMinutes,
|
sectionPlannedMinutes,
|
||||||
} from '../utils/trainingUnitSectionsForm'
|
} from '../utils/trainingUnitSectionsForm'
|
||||||
|
import { PLANNING_MODULE_UX_MODE } from '../config/planningModuleUx'
|
||||||
|
|
||||||
const DND_TU_ITEM = 'application/x-shinkan-training-unit-item'
|
const DND_TU_ITEM = 'application/x-shinkan-training-unit-item'
|
||||||
const DND_TU_SECTION = 'application/x-shinkan-training-section-v1'
|
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 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 (
|
||||||
|
<span
|
||||||
|
className="tu-planning-mod-tag"
|
||||||
|
style={{ borderColor: p.border, backgroundColor: p.soft }}
|
||||||
|
title={`${fullTitle} (Bibliotheks-ID ${moduleId})`}
|
||||||
|
>
|
||||||
|
<span className="tu-planning-mod-tag__dot" style={{ background: p.border }} aria-hidden />
|
||||||
|
<span className="tu-planning-mod-tag__text">Aus Modul: {lbl}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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) {
|
function renderModulePlanningHead(modBandTitle, modOutline, showModuleBand) {
|
||||||
if (!showModuleBand || !modOutline) return null
|
if (!showModuleBand || !modOutline) return null
|
||||||
return (
|
return (
|
||||||
|
|
@ -515,6 +571,7 @@ export default function TrainingUnitSectionsEditor({
|
||||||
{list.map((sec, sIdx) => {
|
{list.map((sec, sIdx) => {
|
||||||
const planMin = sectionPlannedMinutes(sec)
|
const planMin = sectionPlannedMinutes(sec)
|
||||||
const itemCount = sec.items?.length ?? 0
|
const itemCount = sec.items?.length ?? 0
|
||||||
|
const moduleLegend = PLANNING_USE_COMPACT_LEGEND ? sectionModuleLegendModel(sec.items) : []
|
||||||
const bandActiveBefore = (bx) =>
|
const bandActiveBefore = (bx) =>
|
||||||
enableSectionDragReorder &&
|
enableSectionDragReorder &&
|
||||||
dropSectionBand &&
|
dropSectionBand &&
|
||||||
|
|
@ -656,8 +713,21 @@ export default function TrainingUnitSectionsEditor({
|
||||||
(curMn != null ? `Modul #${curMn}` : '')
|
(curMn != null ? `Modul #${curMn}` : '')
|
||||||
|
|
||||||
const modOutline =
|
const modOutline =
|
||||||
showModuleBand && curMn != null ? gatherPlanningModuleOutline(sec.items, iIdx, curMn) : null
|
!PLANNING_USE_COMPACT_LEGEND &&
|
||||||
const fromModClass = curMn != null ? ' tu-item-row--from-module' : ''
|
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') {
|
if (it.item_type === 'note') {
|
||||||
const isSepLine = (it.note_body || '').trim() === SECTION_INSERT_SEPARATOR_BODY
|
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
|
const noteHasText = Boolean((it.note_body || '').trim()) && !isSepLine
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
||||||
{renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
{!PLANNING_USE_COMPACT_LEGEND &&
|
||||||
|
renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`${rowCommon} tu-item-row--note` +
|
`${rowCommon} tu-item-row--note` +
|
||||||
|
|
@ -673,6 +744,7 @@ export default function TrainingUnitSectionsEditor({
|
||||||
fromModClass
|
fromModClass
|
||||||
}
|
}
|
||||||
{...dndRowProps}
|
{...dndRowProps}
|
||||||
|
style={modBorderVarStyle}
|
||||||
>
|
>
|
||||||
{enableItemDragReorder ? (
|
{enableItemDragReorder ? (
|
||||||
<span
|
<span
|
||||||
|
|
@ -706,6 +778,9 @@ export default function TrainingUnitSectionsEditor({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="tu-item-row__body tu-item-row__body--note">
|
<div className="tu-item-row__body tu-item-row__body--note">
|
||||||
|
{!isSepLine && PLANNING_USE_COMPACT_LEGEND && curMn ? (
|
||||||
|
<PlanningModuleRowTag moduleId={curMn} title={modBandTitle} />
|
||||||
|
) : null}
|
||||||
<span className="tu-item-row__meta-label">
|
<span className="tu-item-row__meta-label">
|
||||||
{isSepLine ? 'Trennung' : 'Zwischen-Anmerkung'}
|
{isSepLine ? 'Trennung' : 'Zwischen-Anmerkung'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -768,8 +843,13 @@ export default function TrainingUnitSectionsEditor({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
||||||
{renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
{!PLANNING_USE_COMPACT_LEGEND &&
|
||||||
<div className={`${rowCommon} tu-item-row--exercise${fromModClass}`} {...dndRowProps}>
|
renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
||||||
|
<div
|
||||||
|
className={`${rowCommon} tu-item-row--exercise${fromModClass}`}
|
||||||
|
{...dndRowProps}
|
||||||
|
style={modBorderVarStyle}
|
||||||
|
>
|
||||||
<div className="tu-item-row__mainline">
|
<div className="tu-item-row__mainline">
|
||||||
{enableItemDragReorder ? (
|
{enableItemDragReorder ? (
|
||||||
<span
|
<span
|
||||||
|
|
@ -809,6 +889,9 @@ export default function TrainingUnitSectionsEditor({
|
||||||
) : (
|
) : (
|
||||||
<span className="tu-ex-title-placeholder">Keine Übung gewählt</span>
|
<span className="tu-ex-title-placeholder">Keine Übung gewählt</span>
|
||||||
)}
|
)}
|
||||||
|
{PLANNING_USE_COMPACT_LEGEND && curMn ? (
|
||||||
|
<PlanningModuleRowTag moduleId={curMn} title={modBandTitle} />
|
||||||
|
) : null}
|
||||||
<span className="tu-ex-inline-actions">
|
<span className="tu-ex-inline-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -990,6 +1073,45 @@ export default function TrainingUnitSectionsEditor({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{moduleLegend.length ? (
|
||||||
|
<div
|
||||||
|
className="tu-section-mod-legend"
|
||||||
|
aria-label="Liste der eingebundenen Trainingsmodule in diesem Abschnitt"
|
||||||
|
>
|
||||||
|
<div className="tu-section-mod-legend__caption">Übernommene Module im Abschnitt</div>
|
||||||
|
<ul className="tu-section-mod-legend__list">
|
||||||
|
{moduleLegend.map((e) => {
|
||||||
|
const pal = planningModulePalette(e.id)
|
||||||
|
return (
|
||||||
|
<li key={`mod-leg-${sIdx}-${e.id}`} className="tu-section-mod-legend__item">
|
||||||
|
<span
|
||||||
|
className="tu-section-mod-legend__swatch"
|
||||||
|
style={{ background: pal.border }}
|
||||||
|
title={`Farbe wie an den Zeilen (Modul #${e.id})`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className="tu-section-mod-legend__text">
|
||||||
|
<span className="tu-section-mod-legend__title">
|
||||||
|
{(e.title || '').trim() || `Modul #${e.id}`}
|
||||||
|
</span>
|
||||||
|
<span className="tu-section-mod-legend__meta">
|
||||||
|
ID {e.id} · {e.exercises}{' '}
|
||||||
|
{e.exercises === 1 ? 'Übung' : 'Übungen'}
|
||||||
|
{e.notes > 0 ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
· {e.notes} {e.notes === 1 ? 'Zwischen-Hinweis' : 'Zwischen-Hinweise'}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
11
frontend/src/config/planningModuleUx.js
Normal file
11
frontend/src/config/planningModuleUx.js
Normal file
|
|
@ -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'
|
||||||
Loading…
Reference in New Issue
Block a user