Anmerkung 5, jetzt mit Ursache statt mit Vermutung. SlideOver bleibt geschlossen im DOM — absichtlich, sonst gaebe es keine Schiebebewegung. Geschlossen steht das Panel per translate-x-full hinter dem rechten Bildschirmrand, die eigene linke Kante genau auf dem Rand. Der Schatten ist das Einzige, was von dort noch ins Bild reicht: 32 px Weichzeichnung nach aussen, und aussen heisst hier nach links, in die Seite hinein. Bei einem Panel faellt das nicht auf. Die Protokollseite rendert aber eines **je Zeile** — AuditDetail haengt an jeder. Bei fuenfzig Lagen zu 18 % Deckkraft bleiben rechnerisch 0,01 % Durchsicht: ein fast schwarzer Streifen neben der Bildlaufleiste, mit Stufen dort, wo sich die Weichzeichnungen der einzelnen Lagen ueberlagern. Genau das war zu sehen, und genau deshalb nur auf dieser Seite. Die Tiefe gehoert zum geoeffneten Panel, nicht zum weggeschobenen. Sie erst beim Oeffnen zu setzen kostet nichts. Die Aenderung am Ueberlauf der Tabellenkarten aus dem vorigen Commit bleibt — sie war eine Aufraeumung, die fuer sich steht, aber nicht die Ursache.
83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { X } from "lucide-react";
|
|
import { useId, useRef, type ReactNode } from "react";
|
|
import { Button } from "./Button";
|
|
import { useDialogFocus } from "./useDialogFocus";
|
|
|
|
type SlideOverProps = {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
subtitle?: string;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
};
|
|
|
|
export function SlideOver({ open, onClose, title, subtitle, children, footer }: SlideOverProps) {
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
const titleId = useId();
|
|
useDialogFocus(open, onClose, dialogRef);
|
|
|
|
return (
|
|
// Stays mounted so the slide transition can run, which means a closed
|
|
// panel's fields are still in the tab order — aria-hidden does not remove
|
|
// them. `inert` does, and also blocks clicks, so several closed panels no
|
|
// longer pile up invisible tab stops at the end of the page.
|
|
<div className="fixed inset-0 z-50" inert={!open}>
|
|
<div
|
|
className={`absolute inset-0 bg-black/40 transition-opacity ${open ? "opacity-100" : "opacity-0"}`}
|
|
onClick={onClose}
|
|
aria-hidden
|
|
/>
|
|
<div
|
|
ref={dialogRef}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby={titleId}
|
|
tabIndex={-1}
|
|
// ── Warum der Schatten am geschlossenen Panel wegfällt ──────────
|
|
//
|
|
// Geschlossen steht das Panel per `translate-x-full` genau hinter dem
|
|
// rechten Bildschirmrand: die eigene linke Kante liegt auf dem Rand.
|
|
// Der Schatten ist das Einzige, was von dort noch ins Bild reicht —
|
|
// 32 px Weichzeichnung nach aussen, und „aussen" heisst hier: nach
|
|
// links, in die Seite hinein.
|
|
//
|
|
// Bei einem Panel fällt das nicht auf. Die Protokollseite rendert
|
|
// jedoch eines **je Zeile**, also fünfzig übereinander an derselben
|
|
// Stelle. Bei 18 % Deckkraft bleiben nach fünfzig Lagen rechnerisch
|
|
// 0,01 % Durchsicht — ein fast schwarzer Streifen neben der
|
|
// Bildlaufleiste, mit sichtbaren Stufen dort, wo sich die
|
|
// Weichzeichnungen der einzelnen Lagen überlagern. Genau das hat der
|
|
// Kunde gemeldet.
|
|
//
|
|
// Die Tiefe gehört zum geöffneten Panel, nicht zum weggeschobenen.
|
|
// Sie erst beim Öffnen zu setzen kostet nichts und nimmt dem Stapel
|
|
// seine Wirkung.
|
|
className={`absolute right-0 top-0 flex h-dvh w-full max-w-md flex-col bg-white outline-none transition-transform duration-200 ${
|
|
open ? "translate-x-0 shadow-[var(--shadow-overlay)]" : "translate-x-full"
|
|
}`}
|
|
>
|
|
<div className="flex shrink-0 items-start justify-between border-b border-border px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-6 sm:py-4">
|
|
<div className="min-w-0">
|
|
<h2 id={titleId} className="text-lg font-bold text-ink">
|
|
{title}
|
|
</h2>
|
|
{subtitle && <p className="truncate text-sm text-ink-muted">{subtitle}</p>}
|
|
</div>
|
|
<Button variant="icon" onClick={onClose} aria-label="Schließen" className="-mr-1">
|
|
<X className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto overscroll-contain px-4 py-4 sm:px-6">{children}</div>
|
|
{footer && (
|
|
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-border px-4 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-6 sm:py-4">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|