Refactor modal components and enhance FormActionBar functionality
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s

- Updated modal components to utilize a consistent overlay and panel structure, improving layout and responsiveness.
- Enhanced FormActionBar to support short labels for action buttons, improving usability in mobile views.
- Introduced new styling for action buttons and modal titles, ensuring better alignment and visual consistency across forms.
- Improved accessibility by adding aria-labels and titles to buttons for better screen reader support.
This commit is contained in:
Lars 2026-05-19 10:20:49 +02:00
parent f15aa7c415
commit c9175bd2fd
5 changed files with 206 additions and 93 deletions

View File

@ -1186,56 +1186,203 @@ a.analysis-split__nav-item {
.form-action-bar__inner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
align-items: stretch;
justify-content: flex-start;
gap: 8px;
max-width: min(1100px, 100%);
margin: 0 auto;
}
.form-action-bar__spacer {
flex: 0 0 auto;
min-width: 0;
}
.form-action-bar__primary-group {
display: flex;
flex-wrap: wrap;
align-items: center;
align-items: stretch;
justify-content: flex-end;
gap: 8px;
margin-left: auto;
min-width: 0;
flex: 1 1 auto;
}
.form-action-bar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 44px;
padding: 8px 14px;
font-size: 0.88rem;
font-weight: 600;
white-space: nowrap;
min-width: 0;
}
.form-action-bar__btn--cancel {
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 {
display: flex;
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;
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 {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.modal-form-shell__body {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior-x: none;
-webkit-overflow-scrolling: touch;
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 {
display: flex;
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.
* 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({
placement = 'bottom',
variant = 'default',
saving = false,
saveLabel,
saveShortLabel,
saveAndCloseLabel = 'Speichern & schließen',
saveAndCloseShortLabel,
cancelLabel = 'Abbrechen',
onSave,
onSaveAndClose,
@ -20,6 +39,8 @@ export default function FormActionBar({
primaryIsSaveOnly = false,
}) {
const labelSave = saveLabel ?? (isNew ? 'Anlegen' : 'Speichern')
const shortSave = saveShortLabel ?? (isNew ? 'Neu' : 'Sichern')
const shortClose = saveAndCloseShortLabel ?? 'Fertig'
const showSaveBtn = showSave && (Boolean(onSave) || Boolean(formId))
const showCloseBtn = showSaveAndClose && (Boolean(onSaveAndClose) || Boolean(formId))
const showCancelBtn = showCancel && Boolean(onCancel)
@ -33,9 +54,13 @@ export default function FormActionBar({
primaryIsSaveOnly ? ' btn-secondary' : ' btn-primary'
}`
const primaryCount = (showSaveBtn ? 1 : 0) + (showCloseBtn ? 1 : 0)
return (
<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"
aria-label="Formularaktionen"
>
@ -46,12 +71,12 @@ export default function FormActionBar({
className="btn btn-secondary form-action-bar__btn form-action-bar__btn--cancel"
onClick={onCancel}
disabled={saving}
aria-label={cancelLabel}
title={cancelLabel}
>
{cancelLabel}
<ActionLabel Icon={X} long={cancelLabel} short="" saving={saving} />
</button>
) : (
<span className="form-action-bar__spacer" aria-hidden />
)}
) : null}
<div className="form-action-bar__primary-group">
{showSaveBtn ? (
<button
@ -60,8 +85,9 @@ export default function FormActionBar({
className={saveBtnClass}
disabled={saving}
onClick={onSave || undefined}
title={labelSave}
>
{saving ? 'Speichern…' : labelSave}
<ActionLabel Icon={Save} long={labelSave} short={shortSave} saving={saving} />
</button>
) : null}
{showCloseBtn ? (
@ -71,8 +97,14 @@ export default function FormActionBar({
className={closeBtnClass}
disabled={saving}
onClick={onSaveAndClose || undefined}
title={saveAndCloseLabel}
>
{saving ? 'Speichern…' : saveAndCloseLabel}
<ActionLabel
Icon={Check}
long={saveAndCloseLabel}
short={shortClose}
saving={saving}
/>
</button>
) : null}
</div>

View File

@ -147,30 +147,10 @@ export default function SaveExercisesAsModuleModal({
if (!open) return null
return (
<div
style={{
position: 'fixed',
inset: 0,
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 }}>
<div className="modal-overlay modal-overlay--form modal-overlay--raised">
<div className="card modal-panel--form modal-panel--narrow">
<h2 className="modal-panel__title">Übungen als Trainingsmodul</h2>
<p className="modal-panel__intro">
Es werden die <strong>gespeicherten</strong> Übungspositionen der Einheit vom{' '}
<strong>{unitLabel || '…'}</strong> verwendet. Speichere die Planung vorher, wenn du den aktuellen Stand
brauchst.
@ -296,6 +276,7 @@ export default function SaveExercisesAsModuleModal({
saving={submitting}
showSave={false}
saveAndCloseLabel="Modul anlegen"
saveAndCloseShortLabel="Anlegen"
onCancel={onClose}
/>
</form>

View File

@ -57,37 +57,9 @@ export default function TrainingPlanningUnitFormModal({
const formId = 'planning-unit-form'
return (
<div
data-testid="planning-unit-form-modal"
className="modal-overlay"
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 }}>
<div data-testid="planning-unit-form-modal" className="modal-overlay modal-overlay--form">
<div className="modal-panel--form">
<h2 className="modal-panel__title">
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
</h2>

View File

@ -192,30 +192,10 @@ export default function TrainingPublishToFrameworkModal({
if (!open) return null
return (
<div
style={{
position: 'fixed',
inset: 0,
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 }}>
<div className="modal-overlay modal-overlay--form modal-overlay--raised">
<div className="card modal-panel--form modal-panel--narrow">
<h2 className="modal-panel__title">Ablauf ins Rahmenprogramm übernehmen</h2>
<p className="modal-panel__intro">
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.
</p>
@ -415,6 +395,7 @@ export default function TrainingPublishToFrameworkModal({
saving={submitting}
showSave={false}
saveAndCloseLabel="In Rahmen übernehmen"
saveAndCloseShortLabel="Übernehmen"
onCancel={resetAndClose}
/>
</form>