Files
alpenwerk-hr/components/ui/SlideOver.tsx
Andrei Laas e727fac4b7
Some checks failed
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
CI / Lint, Typen, Tests, Build (push) Has been cancelled
Der Streifen neben der Bildlaufleiste waren fuenfzig Schatten
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.
2026-09-16 22:36:37 +02:00

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>
);
}