fix(ui): Menues und Dialoge ueber Portal ausgeben
Das Statusmenue auf der Projektseite wurde von den Schaltflaechen "Bearbeiten" und "Loeschen" ueberdeckt, war unten abgeschnitten und der Fortschrittswert schien hindurch. Drei Ursachen, alle mit derselben Wurzel: 1. Die Karten setzen overflow-hidden fuer den Akzentstreifen und den Farbverlaufsrand. Ein absolut positioniertes Kind wird daran abgeschnitten. 2. .glass setzt backdrop-filter, die Projektkarte bekommt beim Hovern zusaetzlich ein transform. Beides erzeugt einen Stacking-Context: das z-index des Menues galt nur noch innerhalb der Karte und verlor gegen spaeter gezeichnete Geschwister. Aus demselben Grund bezog sich das "position: fixed" der Klickflaeche zum Schliessen auf die Karte statt auf das Fenster - ein Klick daneben schloss das Menue nicht. 3. .glass hat 3,5 % Deckkraft. Als Kartenflaeche gut, als Overlay unbrauchbar. Der eigene backdrop-filter half nicht, weil er innerhalb eines Vorfahren mit backdrop-filter wirkungslos bleibt. Behoben durch eine neue Popover-Komponente, die per createPortal am body haengt, ihre Position aus dem Ankerelement berechnet und beim Scrollen und bei Groessenaenderungen nachfuehrt. Reicht der Platz nach unten nicht, klappt sie nach oben. Dazu die Klasse .popover mit nahezu deckendem Hintergrund. Betrifft Statusauswahl, Benutzermenue und - aus demselben Grund - den Modal-Dialog: der wird aus Karten heraus geoeffnet und haette sich sonst an der Karte statt am Fenster ausgerichtet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRef, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Check, ChevronDown, Loader2 } from "lucide-react";
|
||||
import type { ProjectStatus } from "@prisma/client";
|
||||
import { Popover } from "@/components/ui/Popover";
|
||||
import { STATUS_META, STATUS_ORDER, statusOf } from "@/lib/status";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -22,6 +23,7 @@ export function StatusSelect({
|
||||
onChanged,
|
||||
}: StatusSelectProps) {
|
||||
const router = useRouter();
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [current, setCurrent] = useState<ProjectStatus>(status);
|
||||
const [pending, startTransition] = useTransition();
|
||||
@@ -54,10 +56,12 @@ export function StatusSelect({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
// Die Karte im Board ist ein Link – der darf hier nicht ausloesen.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpen((value) => !value);
|
||||
@@ -82,60 +86,49 @@ export function StatusSelect({
|
||||
<ChevronDown className="h-3 w-3 opacity-60" />
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Auswahl schliessen"
|
||||
className="fixed inset-0 z-30 cursor-default"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
role="listbox"
|
||||
className="glass fade-up absolute right-0 z-40 mt-2 w-60 overflow-hidden p-1.5"
|
||||
>
|
||||
{STATUS_ORDER.map((value) => {
|
||||
const option = STATUS_META[value];
|
||||
const active = value === current;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void select(value);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors",
|
||||
active ? "bg-white/8" : "hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="mt-1.5 h-2 w-2 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: option.color }}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2 text-sm text-slate-100">
|
||||
{option.label}
|
||||
{active ? <Check className="h-3 w-3 text-violet-300" /> : null}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-[11px] leading-snug text-slate-500">
|
||||
{option.description}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<Popover
|
||||
anchorRef={triggerRef}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
role="listbox"
|
||||
width={264}
|
||||
>
|
||||
{STATUS_ORDER.map((value) => {
|
||||
const option = STATUS_META[value];
|
||||
const active = value === current;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void select(value);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors",
|
||||
active ? "bg-white/8" : "hover:bg-white/6",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="mt-1.5 h-2 w-2 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: option.color }}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2 text-sm text-slate-100">
|
||||
{option.label}
|
||||
{active ? <Check className="h-3 w-3 text-violet-300" /> : null}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-[11px] leading-snug text-slate-500">
|
||||
{option.description}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user