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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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) {
|
||||
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 (
|
||||
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
||||
{renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
||||
{!PLANNING_USE_COMPACT_LEGEND &&
|
||||
renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
||||
<div
|
||||
className={
|
||||
`${rowCommon} tu-item-row--note` +
|
||||
|
|
@ -673,6 +744,7 @@ export default function TrainingUnitSectionsEditor({
|
|||
fromModClass
|
||||
}
|
||||
{...dndRowProps}
|
||||
style={modBorderVarStyle}
|
||||
>
|
||||
{enableItemDragReorder ? (
|
||||
<span
|
||||
|
|
@ -706,6 +778,9 @@ export default function TrainingUnitSectionsEditor({
|
|||
</button>
|
||||
</div>
|
||||
<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">
|
||||
{isSepLine ? 'Trennung' : 'Zwischen-Anmerkung'}
|
||||
</span>
|
||||
|
|
@ -768,8 +843,13 @@ export default function TrainingUnitSectionsEditor({
|
|||
|
||||
return (
|
||||
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
||||
{renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
||||
<div className={`${rowCommon} tu-item-row--exercise${fromModClass}`} {...dndRowProps}>
|
||||
{!PLANNING_USE_COMPACT_LEGEND &&
|
||||
renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
|
||||
<div
|
||||
className={`${rowCommon} tu-item-row--exercise${fromModClass}`}
|
||||
{...dndRowProps}
|
||||
style={modBorderVarStyle}
|
||||
>
|
||||
<div className="tu-item-row__mainline">
|
||||
{enableItemDragReorder ? (
|
||||
<span
|
||||
|
|
@ -809,6 +889,9 @@ export default function TrainingUnitSectionsEditor({
|
|||
) : (
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -990,6 +1073,45 @@ export default function TrainingUnitSectionsEditor({
|
|||
</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>
|
||||
</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