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
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:
parent
f15aa7c415
commit
c9175bd2fd
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user