develop #38

Merged
Lars merged 14 commits from develop into main 2026-05-19 14:56:42 +02:00
5 changed files with 206 additions and 93 deletions
Showing only changes of commit c9175bd2fd - Show all commits

View File

@ -1186,56 +1186,203 @@ a.analysis-split__nav-item {
.form-action-bar__inner { .form-action-bar__inner {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: stretch;
justify-content: space-between; justify-content: flex-start;
gap: 8px; gap: 8px;
max-width: min(1100px, 100%); max-width: min(1100px, 100%);
margin: 0 auto; margin: 0 auto;
}
.form-action-bar__spacer {
flex: 0 0 auto;
min-width: 0; min-width: 0;
} }
.form-action-bar__primary-group { .form-action-bar__primary-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: stretch;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
margin-left: auto; margin-left: auto;
min-width: 0;
flex: 1 1 auto;
} }
.form-action-bar__btn { .form-action-bar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 44px; min-height: 44px;
padding: 8px 14px; padding: 8px 14px;
font-size: 0.88rem; font-size: 0.88rem;
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
min-width: 0;
} }
.form-action-bar__btn--cancel { .form-action-bar__btn--cancel {
min-width: 5.5rem; min-width: 5.5rem;
} }
.form-action-bar__icon {
flex-shrink: 0;
display: none;
}
.form-action-bar__text--short {
display: none;
}
.form-action-bar__saving {
font-size: 0.82rem;
letter-spacing: 0.02em;
}
/* Form-Modale: Overlay + Panel (Desktop zentriert, Mobile Vollbild) */
.modal-overlay--form {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow: hidden;
box-sizing: border-box;
}
.modal-overlay--form.modal-overlay--raised {
z-index: 1100;
}
.modal-panel--form { .modal-panel--form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: min(92vh, 100%); background: var(--surface);
border-radius: 12px;
padding: clamp(12px, 3vw, 2rem);
width: 100%;
max-width: min(1100px, 100%);
max-height: min(92vh, 100dvh);
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
min-width: 0;
box-sizing: border-box;
}
.modal-panel--form.modal-panel--narrow {
max-width: min(560px, 100%);
}
.modal-panel--form.card {
/* card-Klasse nur für Desktop-Rahmen; Mobile überschreibt unten */
}
.modal-panel__title {
margin: 0 0 1rem;
flex-shrink: 0;
font-size: clamp(1.05rem, 4vw, 1.25rem);
line-height: 1.25;
min-width: 0;
}
.modal-panel__intro {
font-size: 0.88rem;
color: var(--text2);
line-height: 1.45;
margin: 0 0 1rem;
flex-shrink: 0;
min-width: 0;
} }
.modal-form-shell { .modal-form-shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
min-width: 0;
overflow: hidden; overflow: hidden;
} }
.modal-form-shell__body { .modal-form-shell__body {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
min-width: 0;
overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
overscroll-behavior-x: none;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
padding-bottom: 4px; padding-bottom: 4px;
} }
.modal-panel--form > *,
.modal-form-shell__body > * {
max-width: 100%;
min-width: 0;
}
@media (max-width: 639px) {
.modal-overlay--form {
padding: 0;
align-items: stretch;
}
.modal-panel--form {
max-width: 100%;
max-height: 100dvh;
height: 100dvh;
border-radius: 0;
padding: 12px;
padding-top: max(12px, env(safe-area-inset-top, 0px));
padding-bottom: max(0px, env(safe-area-inset-bottom, 0px));
}
.modal-panel--form.card {
border: none;
border-radius: 0;
padding: 12px;
padding-top: max(12px, env(safe-area-inset-top, 0px));
padding-bottom: max(0px, env(safe-area-inset-bottom, 0px));
}
.modal-panel__title {
margin-bottom: 0.75rem;
font-size: 1.05rem;
}
.modal-panel__intro {
margin-bottom: 0.75rem;
font-size: 0.85rem;
}
.form-action-bar {
padding: 5px 6px;
padding-bottom: max(5px, env(safe-area-inset-bottom, 0px));
}
.form-action-bar__inner {
flex-wrap: nowrap;
gap: 6px;
max-width: none;
}
.form-action-bar__primary-group {
flex: 1 1 0;
flex-wrap: nowrap;
gap: 6px;
margin-left: 0;
min-width: 0;
}
.form-action-bar__btn {
min-height: 38px;
padding: 6px 8px;
font-size: 0.75rem;
gap: 4px;
flex: 1 1 0;
}
.form-action-bar__btn--cancel {
flex: 0 0 38px;
width: 38px;
min-width: 38px;
max-width: 38px;
padding: 0;
}
.form-action-bar__btn--cancel .form-action-bar__text--long,
.form-action-bar__btn--cancel .form-action-bar__text--short {
display: none !important;
}
.form-action-bar__icon {
display: inline;
}
.form-action-bar__text--long {
display: none;
}
.form-action-bar__text--short {
display: inline;
}
.form-action-bar--single-primary .form-action-bar__primary-group .form-action-bar__btn {
flex: 1 1 auto;
}
}
.page-form-shell { .page-form-shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1,13 +1,32 @@
import { Check, Save, X } from 'lucide-react'
/** /**
* Feste Aktionsleiste für Formulare/Modale: Speichern, Speichern & schließen, Abbrechen. * Feste Aktionsleiste für Formulare/Modale: Speichern, Speichern & schließen, Abbrechen.
* Bleibt sichtbar (sticky), während der Formularinhalt scrollt. * Bleibt sichtbar (sticky), während der Formularinhalt scrollt.
*/ */
function ActionLabel({ Icon, long, short, saving, savingShort = '…' }) {
if (saving) {
return <span className="form-action-bar__saving">{savingShort}</span>
}
return (
<>
{Icon ? <Icon size={17} strokeWidth={2.25} className="form-action-bar__icon" aria-hidden /> : null}
<span className="form-action-bar__text form-action-bar__text--long">{long}</span>
{short != null && short !== '' ? (
<span className="form-action-bar__text form-action-bar__text--short">{short}</span>
) : null}
</>
)
}
export default function FormActionBar({ export default function FormActionBar({
placement = 'bottom', placement = 'bottom',
variant = 'default', variant = 'default',
saving = false, saving = false,
saveLabel, saveLabel,
saveShortLabel,
saveAndCloseLabel = 'Speichern & schließen', saveAndCloseLabel = 'Speichern & schließen',
saveAndCloseShortLabel,
cancelLabel = 'Abbrechen', cancelLabel = 'Abbrechen',
onSave, onSave,
onSaveAndClose, onSaveAndClose,
@ -20,6 +39,8 @@ export default function FormActionBar({
primaryIsSaveOnly = false, primaryIsSaveOnly = false,
}) { }) {
const labelSave = saveLabel ?? (isNew ? 'Anlegen' : 'Speichern') const labelSave = saveLabel ?? (isNew ? 'Anlegen' : 'Speichern')
const shortSave = saveShortLabel ?? (isNew ? 'Neu' : 'Sichern')
const shortClose = saveAndCloseShortLabel ?? 'Fertig'
const showSaveBtn = showSave && (Boolean(onSave) || Boolean(formId)) const showSaveBtn = showSave && (Boolean(onSave) || Boolean(formId))
const showCloseBtn = showSaveAndClose && (Boolean(onSaveAndClose) || Boolean(formId)) const showCloseBtn = showSaveAndClose && (Boolean(onSaveAndClose) || Boolean(formId))
const showCancelBtn = showCancel && Boolean(onCancel) const showCancelBtn = showCancel && Boolean(onCancel)
@ -33,9 +54,13 @@ export default function FormActionBar({
primaryIsSaveOnly ? ' btn-secondary' : ' btn-primary' primaryIsSaveOnly ? ' btn-secondary' : ' btn-primary'
}` }`
const primaryCount = (showSaveBtn ? 1 : 0) + (showCloseBtn ? 1 : 0)
return ( return (
<div <div
className={`form-action-bar form-action-bar--${placement} form-action-bar--${variant}`} className={`form-action-bar form-action-bar--${placement} form-action-bar--${variant}${
primaryCount === 1 ? ' form-action-bar--single-primary' : ''
}`}
role="group" role="group"
aria-label="Formularaktionen" aria-label="Formularaktionen"
> >
@ -46,12 +71,12 @@ export default function FormActionBar({
className="btn btn-secondary form-action-bar__btn form-action-bar__btn--cancel" className="btn btn-secondary form-action-bar__btn form-action-bar__btn--cancel"
onClick={onCancel} onClick={onCancel}
disabled={saving} disabled={saving}
aria-label={cancelLabel}
title={cancelLabel}
> >
{cancelLabel} <ActionLabel Icon={X} long={cancelLabel} short="" saving={saving} />
</button> </button>
) : ( ) : null}
<span className="form-action-bar__spacer" aria-hidden />
)}
<div className="form-action-bar__primary-group"> <div className="form-action-bar__primary-group">
{showSaveBtn ? ( {showSaveBtn ? (
<button <button
@ -60,8 +85,9 @@ export default function FormActionBar({
className={saveBtnClass} className={saveBtnClass}
disabled={saving} disabled={saving}
onClick={onSave || undefined} onClick={onSave || undefined}
title={labelSave}
> >
{saving ? 'Speichern…' : labelSave} <ActionLabel Icon={Save} long={labelSave} short={shortSave} saving={saving} />
</button> </button>
) : null} ) : null}
{showCloseBtn ? ( {showCloseBtn ? (
@ -71,8 +97,14 @@ export default function FormActionBar({
className={closeBtnClass} className={closeBtnClass}
disabled={saving} disabled={saving}
onClick={onSaveAndClose || undefined} onClick={onSaveAndClose || undefined}
title={saveAndCloseLabel}
> >
{saving ? 'Speichern…' : saveAndCloseLabel} <ActionLabel
Icon={Check}
long={saveAndCloseLabel}
short={shortClose}
saving={saving}
/>
</button> </button>
) : null} ) : null}
</div> </div>

View File

@ -147,30 +147,10 @@ export default function SaveExercisesAsModuleModal({
if (!open) return null if (!open) return null
return ( return (
<div <div className="modal-overlay modal-overlay--form modal-overlay--raised">
style={{ <div className="card modal-panel--form modal-panel--narrow">
position: 'fixed', <h2 className="modal-panel__title">Übungen als Trainingsmodul</h2>
inset: 0, <p className="modal-panel__intro">
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1100,
padding: '1rem',
overflowY: 'auto',
}}
>
<div
className="card modal-panel--form"
style={{
maxWidth: 'min(560px, 100%)',
width: '100%',
padding: '1.25rem',
maxHeight: '90vh',
}}
>
<h2 style={{ marginTop: 0, marginBottom: '0.65rem', flexShrink: 0 }}>Übungen als Trainingsmodul</h2>
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.45, marginBottom: '1rem', flexShrink: 0 }}>
Es werden die <strong>gespeicherten</strong> Übungspositionen der Einheit vom{' '} Es werden die <strong>gespeicherten</strong> Übungspositionen der Einheit vom{' '}
<strong>{unitLabel || '…'}</strong> verwendet. Speichere die Planung vorher, wenn du den aktuellen Stand <strong>{unitLabel || '…'}</strong> verwendet. Speichere die Planung vorher, wenn du den aktuellen Stand
brauchst. brauchst.
@ -296,6 +276,7 @@ export default function SaveExercisesAsModuleModal({
saving={submitting} saving={submitting}
showSave={false} showSave={false}
saveAndCloseLabel="Modul anlegen" saveAndCloseLabel="Modul anlegen"
saveAndCloseShortLabel="Anlegen"
onCancel={onClose} onCancel={onClose}
/> />
</form> </form>

View File

@ -57,37 +57,9 @@ export default function TrainingPlanningUnitFormModal({
const formId = 'planning-unit-form' const formId = 'planning-unit-form'
return ( return (
<div <div data-testid="planning-unit-form-modal" className="modal-overlay modal-overlay--form">
data-testid="planning-unit-form-modal" <div className="modal-panel--form">
className="modal-overlay" <h2 className="modal-panel__title">
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '1rem',
}}
>
<div
className="modal-panel--form"
style={{
background: 'var(--surface)',
borderRadius: '12px',
padding: 'clamp(12px, 3vw, 2rem)',
maxWidth: 'min(1100px, 100%)',
width: '100%',
margin: 'max(0px, env(safe-area-inset-top, 0px)) auto',
boxSizing: 'border-box',
minWidth: 0,
}}
>
<h2 style={{ marginBottom: '1rem', flexShrink: 0 }}>
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'} {editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
</h2> </h2>

View File

@ -192,30 +192,10 @@ export default function TrainingPublishToFrameworkModal({
if (!open) return null if (!open) return null
return ( return (
<div <div className="modal-overlay modal-overlay--form modal-overlay--raised">
style={{ <div className="card modal-panel--form modal-panel--narrow">
position: 'fixed', <h2 className="modal-panel__title">Ablauf ins Rahmenprogramm übernehmen</h2>
inset: 0, <p className="modal-panel__intro">
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1100,
padding: '1rem',
overflowY: 'auto',
}}
>
<div
className="card modal-panel--form"
style={{
maxWidth: 'min(520px, 100%)',
width: '100%',
padding: '1.25rem',
maxHeight: '90vh',
}}
>
<h2 style={{ marginTop: 0, marginBottom: '0.65rem', flexShrink: 0 }}>Ablauf ins Rahmenprogramm übernehmen</h2>
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.45, marginBottom: '1rem', flexShrink: 0 }}>
Es wird der <strong>zuletzt gespeicherte</strong> Ablauf dieser Einheit aus der Datenbank übernommen. Es wird der <strong>zuletzt gespeicherte</strong> Ablauf dieser Einheit aus der Datenbank übernommen.
Nicht gespeicherte Änderungen im Formular sind nicht enthalten bitte vorher die Einheit speichern. Nicht gespeicherte Änderungen im Formular sind nicht enthalten bitte vorher die Einheit speichern.
</p> </p>
@ -415,6 +395,7 @@ export default function TrainingPublishToFrameworkModal({
saving={submitting} saving={submitting}
showSave={false} showSave={false}
saveAndCloseLabel="In Rahmen übernehmen" saveAndCloseLabel="In Rahmen übernehmen"
saveAndCloseShortLabel="Übernehmen"
onCancel={resetAndClose} onCancel={resetAndClose}
/> />
</form> </form>