// components/ui/Modal.tsx 'use client'; import * as React from 'react'; import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, } from '@headlessui/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; export type ModalTone = 'default' | 'success' | 'danger' | 'warning' | 'info'; export type ModalVariant = 'centered' | 'alert'; export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'; export interface ModalAction { label: string; onClick?: () => void; variant?: 'primary' | 'secondary' | 'danger'; autoFocus?: boolean; } export interface ModalProps { open: boolean; onClose: () => void; title?: React.ReactNode; description?: React.ReactNode; children?: React.ReactNode; /** Icon (z.B. oder ) */ icon?: React.ReactNode; /** Steuert Icon-Farben/Hintergrund */ tone?: ModalTone; /** Layout: "centered" (Payment successful) oder "alert" (Deactivate account) */ variant?: ModalVariant; /** Breite des Dialogs */ size?: ModalSize; /** X-Button in der Ecke rechts oben */ showCloseButton?: boolean; /** Standard-Buttons im Footer */ primaryAction?: ModalAction; secondaryAction?: ModalAction; /** Grauer Footer-Bereich wie im letzten Beispiel */ useGrayFooter?: boolean; /** * Wenn gesetzt, wird dieser Footer anstelle der auto-generierten Buttons gerendert. * Damit kannst du komplett eigene Layouts bauen. */ footer?: React.ReactNode; /** * Optionaler Inhalt für eine rechte Sidebar (z.B. Geräthistorie). * Auf kleinen Screens unten angehängt, ab sm rechts als Spalte. */ sidebar?: React.ReactNode; } /* ───────── Layout-Helfer ───────── */ const toneStyles: Record< ModalTone, { iconBg: string; iconColor: string } > = { default: { iconBg: 'bg-gray-100 dark:bg-gray-500/10', iconColor: 'text-gray-600 dark:text-gray-400', }, success: { iconBg: 'bg-green-100 dark:bg-green-500/10', iconColor: 'text-green-600 dark:text-green-400', }, danger: { iconBg: 'bg-red-100 dark:bg-red-500/10', iconColor: 'text-red-600 dark:text-red-400', }, warning: { iconBg: 'bg-yellow-100 dark:bg-yellow-500/10', iconColor: 'text-yellow-600 dark:text-yellow-400', }, info: { iconBg: 'bg-sky-100 dark:bg-sky-500/10', iconColor: 'text-sky-600 dark:text-sky-400', }, }; const sizeClasses: Record = { sm: 'sm:max-w-sm', md: 'sm:max-w-lg', lg: 'sm:max-w-3xl', // ein bisschen breiter für Sidebar xl: 'sm:max-w-5xl', // ein bisschen breiter für Sidebar }; const baseButtonClasses = 'inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-xs ' + 'focus-visible:outline-2 focus-visible:outline-offset-2 '; const buttonVariantClasses: Record< NonNullable, string > = { primary: 'bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:outline-indigo-600 ' + 'dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500 dark:shadow-none', secondary: 'bg-white text-gray-900 inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 ' + 'dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20', danger: 'bg-red-600 text-white hover:bg-red-500 focus-visible:outline-red-600 ' + 'dark:bg-red-500 dark:hover:bg-red-400 dark:shadow-none', }; function renderActionButton( action: ModalAction, extraClasses?: string, ): React.ReactNode { const variant = action.variant ?? 'primary'; return ( ); } /* ───────── Modal-Komponente ───────── */ export function Modal({ open, onClose, title, description, children, icon, tone = 'default', variant = 'centered', size = 'md', showCloseButton = false, primaryAction, secondaryAction, useGrayFooter = false, footer, sidebar, }: ModalProps) { const toneStyle = toneStyles[tone]; const panelSizeClasses = sizeClasses[size]; const hasActions = !!primaryAction || !!secondaryAction; const isAlert = variant === 'alert'; const bodyContent = children ?? description; return (
{/* X-Button oben rechts (optional) */} {showCloseButton && (
)} {/* Header + Body + Sidebar */}
{/* Header (Icon + Titel + optionale Beschreibung) */}
{icon && (
)}
{title && ( {title} )} {/* Beschreibung nur anzeigen, wenn keine eigenen Children übergeben wurden */} {!children && description && (

{description}

)}
{/* Body + Sidebar */ } {(bodyContent || sidebar) && (
{bodyContent && (
{bodyContent}
)} {sidebar && ( )}
)}
{/* Footer */} {footer ? ( footer ) : hasActions ? (
{primaryAction && renderActionButton( primaryAction, isAlert ? 'sm:w-auto' : secondaryAction ? 'sm:col-start-2' : '', )} {secondaryAction && renderActionButton( { ...secondaryAction, variant: secondaryAction.variant ?? 'secondary' }, clsx( 'mt-3 sm:mt-0', isAlert && 'sm:w-auto sm:mr-3', !useGrayFooter && 'bg-white', ), )}
) : null}
); } export default Modal;