Update-Banner: klebt oben fest (sticky), Sticky-Header rasten darunter ein

Der Hinweis "Neue Version verfuegbar" scrollte bisher weg. Jetzt sticky an
der Viewport-Oberkante; das Banner meldet seine gemessene Hoehe als CSS-Var
--app-banner-h, an der sich die Sticky-Header von ContractDetail/ContractForm
ausrichten (top-[var(--app-banner-h,0px)]), damit sie nicht dahinter
verschwinden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 12:20:31 +02:00
co-authored by Claude Opus 4.8
parent e1ca56ebfb
commit aff3f7e84a
3 changed files with 37 additions and 5 deletions
+35 -3
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { RefreshCw, X } from 'lucide-react'; import { RefreshCw, X } from 'lucide-react';
import { useVersionCheck } from '../hooks/useVersionCheck'; import { useVersionCheck } from '../hooks/useVersionCheck';
@@ -7,15 +7,47 @@ import { useVersionCheck } from '../hooks/useVersionCheck';
* Version verfügbar ist (siehe useVersionCheck). "Jetzt neu laden" lädt die * Version verfügbar ist (siehe useVersionCheck). "Jetzt neu laden" lädt die
* Seite neu und holt damit den frischen Code; "später" blendet für die * Seite neu und holt damit den frischen Code; "später" blendet für die
* laufende Session aus. * laufende Session aus.
*
* Das Banner klebt (`sticky top-0`) an der Viewport-Oberkante, bleibt beim
* Scrollen also stehen. Damit die seitenspezifischen Sticky-Header
* (ContractDetail/ContractForm mit `top-[var(--app-banner-h)]`) nicht darunter
* verschwinden, meldet das Banner seine gemessene Höhe als CSS-Variable
* `--app-banner-h` am <html>-Element; ohne Banner ist sie 0px.
*/ */
export default function UpdateBanner() { export default function UpdateBanner() {
const updateAvailable = useVersionCheck(); const updateAvailable = useVersionCheck();
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
const barRef = useRef<HTMLDivElement>(null);
if (!updateAvailable || dismissed) return null; const visible = updateAvailable && !dismissed;
useLayoutEffect(() => {
const root = document.documentElement;
if (!visible) {
root.style.setProperty('--app-banner-h', '0px');
return;
}
const el = barRef.current;
if (!el) return;
const apply = () => root.style.setProperty('--app-banner-h', `${el.offsetHeight}px`);
apply();
const ro = new ResizeObserver(apply);
ro.observe(el);
return () => {
ro.disconnect();
root.style.setProperty('--app-banner-h', '0px');
};
}, [visible]);
// Sicherstellen, dass die Variable auch beim Unmount zurückgesetzt wird.
useEffect(() => () => {
document.documentElement.style.setProperty('--app-banner-h', '0px');
}, []);
if (!visible) return null;
return ( return (
<div className="bg-blue-600 text-white px-6 py-2.5"> <div ref={barRef} className="sticky top-0 z-40 bg-blue-600 text-white px-6 py-2.5">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<RefreshCw className="w-4 h-4 flex-shrink-0" /> <RefreshCw className="w-4 h-4 flex-shrink-0" />
@@ -1828,7 +1828,7 @@ export default function ContractDetail() {
{/* Sticky-Header: Vertragsnummer + Kunde + Aktionsbuttons bleiben {/* Sticky-Header: Vertragsnummer + Kunde + Aktionsbuttons bleiben
beim Scrollen oben stehen. -mx-8 px-8 zieht den Balken auf die beim Scrollen oben stehen. -mx-8 px-8 zieht den Balken auf die
Layout-Padding-Kante, damit's optisch bündig ist. */} 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 className="sticky top-[var(--app-banner-h,0px)] 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>
<div className="flex items-center gap-4 mb-2"> <div className="flex items-center gap-4 mb-2">
<Button <Button
@@ -860,7 +860,7 @@ export default function ContractForm() {
{/* Sticky-Header: Zurück-Button + Titel + Kunde-Zeile bleiben beim {/* Sticky-Header: Zurück-Button + Titel + Kunde-Zeile bleiben beim
Scrollen oben stehen. Layout-Padding wird per -mx-8 px-8 Scrollen oben stehen. Layout-Padding wird per -mx-8 px-8
überbrückt, damit der Balken bündig ist. */} ü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="sticky top-[var(--app-banner-h,0px)] 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"> <div className="flex items-center gap-4 mb-2">
<Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}> <Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}>
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />