149 lines
3.7 KiB
TypeScript
149 lines
3.7 KiB
TypeScript
import React, { ReactNode } from 'react';
|
|
import { Box } from './primitives/Box';
|
|
import { Stack } from './primitives/Stack';
|
|
import { Button } from './Button';
|
|
import { Text } from './Text';
|
|
import { X } from 'lucide-react';
|
|
import { IconButton } from './IconButton';
|
|
|
|
interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose?: () => void;
|
|
onOpenChange?: (open: boolean) => void;
|
|
title?: string;
|
|
description?: string;
|
|
icon?: React.ReactNode;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
primaryActionLabel?: string;
|
|
onPrimaryAction?: () => void;
|
|
secondaryActionLabel?: string;
|
|
onSecondaryAction?: () => void;
|
|
isLoading?: boolean;
|
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
}
|
|
|
|
export function Modal({
|
|
isOpen,
|
|
onClose,
|
|
onOpenChange,
|
|
title,
|
|
description,
|
|
icon,
|
|
children,
|
|
footer,
|
|
primaryActionLabel,
|
|
onPrimaryAction,
|
|
secondaryActionLabel,
|
|
onSecondaryAction,
|
|
isLoading = false,
|
|
size = 'md',
|
|
}: ModalProps) {
|
|
if (!isOpen) return null;
|
|
|
|
const sizeMap = {
|
|
sm: 'max-w-md',
|
|
md: 'max-w-lg',
|
|
lg: 'max-w-2xl',
|
|
xl: 'max-w-4xl',
|
|
};
|
|
|
|
const handleClose = () => {
|
|
if (onClose) onClose();
|
|
if (onOpenChange) onOpenChange(false);
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
position="fixed"
|
|
inset={0}
|
|
zIndex={60}
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
bg="bg-black/60"
|
|
px={4}
|
|
className="backdrop-blur-sm"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
{/* Backdrop click to close */}
|
|
<Box position="absolute" inset={0} onClick={handleClose} />
|
|
|
|
<Box
|
|
position="relative"
|
|
w="full"
|
|
maxWidth={sizeMap[size]}
|
|
rounded="2xl"
|
|
bg="bg-[#0f1115]"
|
|
border
|
|
borderColor="border-[#262626]"
|
|
shadow="2xl"
|
|
overflow="hidden"
|
|
tabIndex={-1}
|
|
>
|
|
{/* Header */}
|
|
<Box p={6} borderBottom borderColor="border-white/5">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Stack direction="row" align="center" gap={3}>
|
|
{icon && <Box>{icon}</Box>}
|
|
<Box>
|
|
{title && (
|
|
<Text size="xl" weight="bold" color="text-white" block>
|
|
{title}
|
|
</Text>
|
|
)}
|
|
{description && (
|
|
<Text size="sm" color="text-gray-400" block mt={1}>
|
|
{description}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Stack>
|
|
<IconButton
|
|
icon={X}
|
|
onClick={handleClose}
|
|
variant="ghost"
|
|
size="sm"
|
|
title="Close modal"
|
|
/>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box p={6} overflowY="auto" maxHeight="calc(100vh - 200px)">
|
|
{children}
|
|
</Box>
|
|
|
|
{/* Footer */}
|
|
{(primaryActionLabel || secondaryActionLabel || footer) && (
|
|
<Box p={6} borderTop borderColor="border-white/5">
|
|
{footer || (
|
|
<Stack direction="row" justify="end" gap={3}>
|
|
{secondaryActionLabel && (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={onSecondaryAction || onClose}
|
|
disabled={isLoading}
|
|
>
|
|
{secondaryActionLabel}
|
|
</Button>
|
|
)}
|
|
{primaryActionLabel && (
|
|
<Button
|
|
variant="primary"
|
|
onClick={onPrimaryAction}
|
|
isLoading={isLoading}
|
|
>
|
|
{primaryActionLabel}
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|