/** * Dialoge. * * Zwei Bauarten, und der Unterschied ist fachlich: * * - `Dialog` fuer Eingaben (anlegen, bearbeiten). Er laesst sich mit Escape und * einem Klick daneben schliessen. * - `ConfirmDialog` fuer Handlungen, die etwas veraendern. Er verlangt eine * ausdrueckliche Bestaetigung. Bei zerstoerenden Handlungen kann er zusaetzlich * ein woertlich einzutippendes Wort verlangen — dasselbe Mittel, das * `uninstall.sh` und die Wiederherstellung mit Ueberschreiben verwenden. Ein * versehentlich gesetztes Kennzeichen in einem Skript reicht dann nicht aus, * und ein Klick aus Gewohnheit auch nicht. */ import * as DialogPrimitive from '@radix-ui/react-dialog'; import { X } from 'lucide-react'; import { useState, type ReactNode } from 'react'; import { cn } from '@/lib/utils'; import { Button } from './Button'; export const DialogRoot = DialogPrimitive.Root; export const DialogTrigger = DialogPrimitive.Trigger; export const DialogClose = DialogPrimitive.Close; export function DialogContent({ className, children, size = 'md', }: { readonly className?: string | undefined; readonly children: ReactNode; readonly size?: 'sm' | 'md' | 'lg' | 'xl' | undefined; }) { const sizeClasses = { sm: 'max-w-md', md: 'max-w-lg', lg: 'max-w-2xl', xl: 'max-w-4xl', } as const; return ( {children} ); } export function DialogHeader({ title, description, }: { readonly title: ReactNode; readonly description?: ReactNode | undefined; }) { return (
{title} {description ? ( {description} ) : null}
); } export function DialogBody({ className, children, }: { readonly className?: string | undefined; readonly children: ReactNode; }) { return
{children}
; } export function DialogFooter({ children }: { readonly children: ReactNode }) { return (
{children}
); } /** * Bestaetigungsdialog fuer veraendernde Handlungen. * * `confirmationWord` ist die zweite Huerde: Wird es gesetzt, muss der Betreiber * es woertlich eintippen. Das gehoert ausschliesslich an Handlungen, die Daten * unwiederbringlich entfernen oder ueberschreiben — ein Ritual ohne Anlass * gewoehnt das Wegklicken an, und dann wirkt es dort nicht mehr, wo es zaehlt. */ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel = 'Bestaetigen', cancelLabel = 'Abbrechen', confirmationWord, confirmationHint, isDestructive = false, isLoading = false, onConfirm, children, }: { readonly open: boolean; readonly onOpenChange: (isOpen: boolean) => void; readonly title: ReactNode; readonly description?: ReactNode | undefined; readonly confirmLabel?: string | undefined; readonly cancelLabel?: string | undefined; /** Wird gesetzt, muss der Betreiber dieses Wort woertlich eintippen. */ readonly confirmationWord?: string | undefined; readonly confirmationHint?: ReactNode | undefined; readonly isDestructive?: boolean | undefined; readonly isLoading?: boolean | undefined; readonly onConfirm: () => void | Promise; readonly children?: ReactNode | undefined; }) { return ( {/* Der Inhalt haengt im Portal und wird beim Schliessen ausgehaengt. Damit verschwindet die eingetippte Bestaetigung von selbst — ein Zuruecksetzen im Effekt braucht es nicht, und die Huerde steht beim naechsten Oeffnen wieder. */} onOpenChange(false)} onConfirm={onConfirm} > {children} ); } /** Inhalt des Bestaetigungsdialogs; haelt die eingetippte Bestaetigung. */ function ConfirmDialogBody({ title, description, confirmLabel, cancelLabel, confirmationWord, confirmationHint, isDestructive, isLoading, onCancel, onConfirm, children, }: { readonly title: ReactNode; readonly description?: ReactNode | undefined; readonly confirmLabel: string; readonly cancelLabel: string; readonly confirmationWord?: string | undefined; readonly confirmationHint?: ReactNode | undefined; readonly isDestructive: boolean; readonly isLoading: boolean; readonly onCancel: () => void; readonly onConfirm: () => void | Promise; readonly children?: ReactNode | undefined; }) { const [typedConfirmation, setTypedConfirmation] = useState(''); const confirmationSatisfied = !confirmationWord || typedConfirmation.trim() === confirmationWord; return ( <> {children} {confirmationWord ? (
setTypedConfirmation(changeEvent.target.value)} className="w-full rounded-md border border-line-strong bg-card px-3 py-2 font-mono text-sm text-fg" />
) : null}
); }