// 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'; import Button from './Button'; 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; disabled?: 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. */ footer?: React.ReactNode; /** * Optionaler Inhalt für eine rechte Sidebar (z.B. Geräthistorie). */ sidebar?: React.ReactNode; /** * Zusätzlicher Inhalt direkt UNTER dem Titel im Header (z.B. Tabs). * Liegt außerhalb des scrollbaren Body-Bereichs. */ headerExtras?: 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', xl: 'sm:max-w-5xl', }; function renderActionButton( action: ModalAction, extraClasses?: string, ): React.ReactNode { const variant = action.variant ?? 'primary'; let buttonVariant: 'primary' | 'secondary' | 'soft' = 'primary'; let tone: 'indigo' | 'gray' | 'rose' = 'indigo'; if (variant === 'secondary') { buttonVariant = 'secondary'; tone = 'gray'; } else if (variant === 'danger') { buttonVariant = 'primary'; tone = 'rose'; } 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, headerExtras, }: ModalProps) { const toneStyle = toneStyles[tone]; const panelSizeClasses = sizeClasses[size]; const hasActions = !!primaryAction || !!secondaryAction; const hasBothActions = !!primaryAction && !!secondaryAction; const isAlert = variant === 'alert'; const bodyContent = children ?? description; return ( {/* Zentrierter Container – keine eigene Scrollbar mehr, die Höhe begrenzt das Panel */}
{/* X-Button oben rechts (optional) */} {showCloseButton && (
)} {/* HEADER + MAIN (Body+Sidebar) – dieser Block bekommt flex-1 und min-h-0 */}
{/* Header (nicht scrollend) */}
{icon && (
)}
{title && ( {title} )} {!children && description && (

{description}

)} {headerExtras && (
{headerExtras}
)}
{/* MAIN: Body + Sidebar – bekommt min-h-0 und nur der Body scrollt */} {(bodyContent || sidebar) && (
{/* Linker Inhalt (scrollbar) */} {bodyContent && (
{bodyContent}
)} {/* Rechte Sidebar (nicht scrollbar, eigene interne Scroller möglich) */} {sidebar && ( )}
)}
{/* Footer (immer sichtbar, außerhalb des scrollbaren Bereichs) */} {footer ? ( footer ) : hasActions ? (
{primaryAction && renderActionButton( primaryAction, hasBothActions ? 'flex-1' : undefined, )} {secondaryAction && renderActionButton( { ...secondaryAction, variant: secondaryAction.variant ?? 'secondary' }, hasBothActions ? 'flex-1' : undefined, )}
) : null}
); } export default Modal;