Vertrag-UI: Sticky-Header + Sticky-Footer

ContractDetail: der obere Balken (Vertragsnummer, Kunde-Link,
Aktions-Buttons wie Folgevertrag/Bearbeiten/Löschen) bleibt beim
Scrollen sichtbar. -mx-8 px-8 überbrückt das Layout-Padding,
damit der Balken bündig sitzt.

ContractForm: Heading + Kunde-Zeile sticky oben, Speichern/
Abbrechen sticky unten. Damit muss man beim langen Formular nicht
mehr nach oben oder unten rollen.

ScrollToTopButton: von bottom-6 auf bottom-24 verschoben, sonst
sitzt der runde Nach-oben-Button beim Runterscrollen genau über
dem Speichern-Button im Sticky-Footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 20:46:15 +02:00
parent b23ebeefc3
commit b48cd72a2b
3 changed files with 56 additions and 40 deletions
@@ -29,10 +29,14 @@ export default function ScrollToTopButton() {
if (!isVisible) return null;
// bottom-24 statt bottom-6: der Nach-oben-Button muss auf Seiten mit
// Sticky-Footer (z.B. ContractForm Speichern/Abbrechen) über dem Footer
// sitzen, sonst hängt er beim Runterscrollen genau über dem Speichern-
// Button. 96px reichen für alle heute vorhandenen Sticky-Footer.
return (
<button
onClick={scrollToTop}
className="fixed bottom-6 right-6 p-3 bg-gray-200 hover:bg-gray-300 text-gray-600 rounded-full shadow-md transition-all duration-300 opacity-70 hover:opacity-100 z-50"
className="fixed bottom-24 right-6 p-3 bg-gray-200 hover:bg-gray-300 text-gray-600 rounded-full shadow-md transition-all duration-300 opacity-70 hover:opacity-100 z-50"
aria-label="Nach oben scrollen"
title="Nach oben"
>
@@ -1795,7 +1795,10 @@ export default function ContractDetail() {
return (
<div>
<div className="flex items-center justify-between mb-6">
{/* Sticky-Header: Vertragsnummer + Kunde + Aktionsbuttons bleiben
beim Scrollen oben stehen. -mx-8 px-8 zieht den Balken auf die
Layout-Padding-Kante, damit's optisch bündig ist. */}
<div className="sticky top-0 z-20 -mx-8 px-8 py-4 mb-6 bg-gray-100 border-b border-gray-200 flex items-center justify-between">
<div>
<div className="flex items-center gap-4 mb-2">
<Button
+47 -38
View File
@@ -777,44 +777,49 @@ export default function ContractForm() {
return (
<div>
<div className="flex items-center gap-4 mb-2">
<Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-2xl font-bold">
{isEdit ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}
</h1>
{/* Sticky-Header: Zurück-Button + Titel + Kunde-Zeile bleiben beim
Scrollen oben stehen. Layout-Padding wird per -mx-8 px-8
überbrückt, damit der Balken bündig ist. */}
<div className="sticky top-0 z-20 -mx-8 px-8 pt-4 pb-3 mb-6 bg-gray-100 border-b border-gray-200">
<div className="flex items-center gap-4 mb-2">
<Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-2xl font-bold">
{isEdit ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}
</h1>
</div>
{customer && (
<p className="text-gray-500 ml-12 flex items-center gap-1">
Kunde:{' '}
<Link
to={`/customers/${customer.id}`}
className="text-blue-600 hover:underline"
>
{customer.companyName || `${customer.firstName} ${customer.lastName}`}
</Link>
<a
href={`/customers/${customer.id}`}
target="_blank"
rel="noopener noreferrer"
title="Kundenakte in neuem Tab öffnen"
aria-label="Kundenakte in neuem Tab öffnen"
className="text-gray-400 hover:text-blue-600 p-1 rounded"
>
<ExternalLink className="w-4 h-4" />
</a>
<button
type="button"
onClick={() => setShowCustomerInfo(true)}
title="Wichtige Kundendaten anzeigen (Schnellansicht mit Copy-Buttons)"
aria-label="Kundendaten anzeigen"
className="text-gray-400 hover:text-blue-600 p-1 rounded"
>
<Info className="w-4 h-4" />
</button>
</p>
)}
</div>
{customer && (
<p className="text-gray-500 ml-12 mb-6 flex items-center gap-1">
Kunde:{' '}
<Link
to={`/customers/${customer.id}`}
className="text-blue-600 hover:underline"
>
{customer.companyName || `${customer.firstName} ${customer.lastName}`}
</Link>
<a
href={`/customers/${customer.id}`}
target="_blank"
rel="noopener noreferrer"
title="Kundenakte in neuem Tab öffnen"
aria-label="Kundenakte in neuem Tab öffnen"
className="text-gray-400 hover:text-blue-600 p-1 rounded"
>
<ExternalLink className="w-4 h-4" />
</a>
<button
type="button"
onClick={() => setShowCustomerInfo(true)}
title="Wichtige Kundendaten anzeigen (Schnellansicht mit Copy-Buttons)"
aria-label="Kundendaten anzeigen"
className="text-gray-400 hover:text-blue-600 p-1 rounded"
>
<Info className="w-4 h-4" />
</button>
</p>
)}
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
@@ -1784,7 +1789,11 @@ export default function ContractForm() {
/>
</Card>
<div className="flex justify-end gap-4">
{/* Sticky-Footer: Speichern/Abbrechen bleiben unten sichtbar,
damit man beim langen Formular nicht ganz nach unten scrollen
muss. -mx-8 px-8 -mb-8 zieht den Balken bündig an die
Layout-Padding-Kante. */}
<div className="sticky bottom-0 z-20 -mx-8 px-8 pt-3 pb-3 -mb-8 mt-6 bg-gray-100 border-t border-gray-200 flex justify-end gap-4">
<Button type="button" variant="secondary" onClick={() => navigate(back.to, { state: back.state })}>
Abbrechen
</Button>