veza/apps/web/src/components/ui/dialog.tsx
senke 40170e188a [FIX] PROD-010: Corriger ENUM PostgreSQL dans modèle User - Tests E2E passent
- Ajout de type:user_role dans le tag GORM du champ Role
- Amélioration de la détection d'erreurs ENUM dans le service Register
- L'endpoint /auth/register retourne maintenant 201 OK avec tokens
- Score production: 52/70 → 58/70
- PROD-010 marqué comme fixed (P0 blocker résolu)
2026-01-04 01:44:19 +01:00

265 lines
5.6 KiB
TypeScript

import React from 'react';
import { Modal } from './modal';
import { Button } from './button';
import { cn } from '@/lib/utils';
import { AlertCircle, Info } from 'lucide-react';
export interface DialogProps {
open: boolean;
onClose?: () => void;
onOpenChange?: (open: boolean) => void;
title?: string;
children: React.ReactNode;
footer?: React.ReactNode;
variant?: 'default' | 'alert' | 'confirm' | 'info';
onConfirm?: () => void | Promise<void>;
onCancel?: () => void;
confirmLabel?: string;
cancelLabel?: string;
showCancel?: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const variantIcons = {
alert: AlertCircle,
confirm: AlertCircle,
info: Info,
default: undefined,
};
const variantStyles = {
alert: 'text-destructive',
confirm: 'text-primary',
info: 'text-blue-600',
default: '',
};
/**
* Composant Dialog avancé avec header, body, footer et actions.
*/
export function Dialog({
open,
onClose,
onOpenChange,
title,
children,
footer,
variant = 'default',
onConfirm,
onCancel,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
showCancel = true,
size = 'md',
}: DialogProps) {
const handleClose = () => {
if (onOpenChange) {
onOpenChange(false);
} else if (onClose) {
onClose();
}
};
const handleConfirm = async () => {
if (onConfirm) {
await onConfirm();
}
handleClose();
};
const handleCancel = () => {
if (onCancel) {
onCancel();
}
handleClose();
};
const IconComponent = variantIcons[variant];
const iconStyle = variantStyles[variant];
return (
<Modal
open={open}
onClose={handleClose}
size={size}
closeOnOverlayClick={variant === 'default'}
>
<div className="flex flex-col">
{/* Header */}
{title && (
<DialogHeader variant={variant}>
<div className="flex items-center gap-3">
{IconComponent && (
<IconComponent className={cn('h-5 w-5', iconStyle)} />
)}
<h2 className="text-2xl font-semibold leading-none tracking-tight">
{title}
</h2>
</div>
</DialogHeader>
)}
{/* Body */}
<DialogBody variant={variant}>{children}</DialogBody>
{/* Footer */}
{footer || onConfirm || onCancel ? (
<DialogFooter>
{footer ? (
footer
) : (
<div className="flex justify-end gap-2">
{showCancel && (
<Button variant="outline" onClick={handleCancel}>
{cancelLabel}
</Button>
)}
{onConfirm && (
<Button
variant={variant === 'alert' ? 'destructive' : 'default'}
onClick={handleConfirm}
>
{confirmLabel}
</Button>
)}
</div>
)}
</DialogFooter>
) : null}
</div>
</Modal>
);
}
export interface DialogHeaderProps {
children: React.ReactNode;
variant?: 'default' | 'alert' | 'confirm' | 'info';
className?: string;
}
export function DialogHeader({
children,
variant: _variant = 'default', // Unused but kept for API compatibility
className,
}: DialogHeaderProps) {
return (
<div
className={cn(
'flex items-center justify-between p-6 border-b',
className,
)}
>
{children}
</div>
);
}
export interface DialogBodyProps {
children: React.ReactNode;
variant?: 'default' | 'alert' | 'confirm' | 'info';
className?: string;
}
export function DialogBody({
children,
variant = 'default',
className,
}: DialogBodyProps) {
return (
<div
className={cn(
'p-6',
variant === 'alert' && 'text-destructive-foreground',
className,
)}
>
{children}
</div>
);
}
export interface DialogFooterProps {
children: React.ReactNode;
className?: string;
}
export function DialogFooter({ children, className }: DialogFooterProps) {
return (
<div
className={cn(
'flex items-center justify-end gap-2 p-6 border-t',
className,
)}
>
{children}
</div>
);
}
// Radix UI-style components for compatibility
export interface DialogContentProps {
children: React.ReactNode;
className?: string;
}
export function DialogContent({ children, className }: DialogContentProps) {
return <div className={cn('p-6', className)}>{children}</div>;
}
export interface DialogDescriptionProps {
children: React.ReactNode;
className?: string;
}
export function DialogDescription({
children,
className,
}: DialogDescriptionProps) {
return (
<p className={cn('text-sm text-muted-foreground', className)}>
{children}
</p>
);
}
export interface DialogTitleProps {
children: React.ReactNode;
className?: string;
}
export function DialogTitle({ children, className }: DialogTitleProps) {
return (
<h2
className={cn(
'text-2xl font-semibold leading-none tracking-tight',
className,
)}
>
{children}
</h2>
);
}
export interface DialogTriggerProps {
children: React.ReactNode;
asChild?: boolean;
onClick?: () => void;
}
export function DialogTrigger({
children,
asChild,
onClick,
}: DialogTriggerProps) {
// If asChild, we expect the child to handle the click
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
onClick: onClick || children.props.onClick,
} as any);
}
return (
<div onClick={onClick} style={{ display: 'inline-block' }}>
{children}
</div>
);
}