chore(agents): Flutter/iOS/Swift-Subagenten aus claude-subagents-Repo übernommen Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
95 lines
3.0 KiB
TypeScript
95 lines
3.0 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
import { Button } from "./button";
|
|
|
|
interface Props {
|
|
offen: boolean;
|
|
titel: string;
|
|
beschreibung?: string;
|
|
bestaetigenText?: string;
|
|
abbrechenText?: string;
|
|
gefaehrlich?: boolean;
|
|
onBestaetigen: () => void;
|
|
onAbbrechen: () => void;
|
|
}
|
|
|
|
// UI2-007: Ersatz für natives window.confirm() - Fokus-Trap (Tab bleibt im
|
|
// Dialog) + Escape-zum-Schließen, große Touch-Buttons. Kein @radix-ui/react-
|
|
// dialog-Paket (Kachel-Vorgabe "Radix-artiges Pattern", kein Framework-
|
|
// Wechsel) - eigenes Portal reicht für den einen Anwendungsfall.
|
|
export function AlertDialog({
|
|
offen,
|
|
titel,
|
|
beschreibung,
|
|
bestaetigenText = "Bestätigen",
|
|
abbrechenText = "Abbrechen",
|
|
gefaehrlich = false,
|
|
onBestaetigen,
|
|
onAbbrechen,
|
|
}: Props) {
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!offen) return;
|
|
const vorherigerFokus = document.activeElement as HTMLElement | null;
|
|
dialogRef.current?.querySelector<HTMLElement>("button")?.focus();
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "Escape") {
|
|
onAbbrechen();
|
|
return;
|
|
}
|
|
if (e.key !== "Tab") return;
|
|
const fokussierbar = dialogRef.current?.querySelectorAll<HTMLElement>("button");
|
|
if (!fokussierbar || fokussierbar.length === 0) return;
|
|
const erstes = fokussierbar[0];
|
|
const letztes = fokussierbar[fokussierbar.length - 1];
|
|
if (e.shiftKey && document.activeElement === erstes) {
|
|
e.preventDefault();
|
|
letztes.focus();
|
|
} else if (!e.shiftKey && document.activeElement === letztes) {
|
|
e.preventDefault();
|
|
erstes.focus();
|
|
}
|
|
}
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => {
|
|
document.removeEventListener("keydown", onKeyDown);
|
|
vorherigerFokus?.focus();
|
|
};
|
|
}, [offen, onAbbrechen]);
|
|
|
|
if (!offen) return null;
|
|
|
|
return createPortal(
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
|
onClick={onAbbrechen}
|
|
>
|
|
<div
|
|
ref={dialogRef}
|
|
role="alertdialog"
|
|
aria-modal="true"
|
|
aria-labelledby="alert-dialog-titel"
|
|
className="w-full max-w-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-lg"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<h2 id="alert-dialog-titel" className="text-base font-semibold text-[var(--color-card-foreground)]">
|
|
{titel}
|
|
</h2>
|
|
{beschreibung && <p className="mt-2 text-sm text-[var(--color-text-muted)]">{beschreibung}</p>}
|
|
<div className="mt-5 flex justify-end gap-2">
|
|
<Button variant="secondary" onClick={onAbbrechen}>
|
|
{abbrechenText}
|
|
</Button>
|
|
<Button variant={gefaehrlich ? "danger" : "default"} onClick={onBestaetigen}>
|
|
{bestaetigenText}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
}
|