147 lines
3.6 KiB
TypeScript
147 lines
3.6 KiB
TypeScript
import React, { ReactNode, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { Box } from './primitives/Box';
|
|
import { Surface } from './primitives/Surface';
|
|
import { IconButton } from './IconButton';
|
|
import { Button } from './Button';
|
|
import { X } from 'lucide-react';
|
|
import { Heading } from './Heading';
|
|
import { Text } from './Text';
|
|
|
|
export interface ModalProps {
|
|
children: ReactNode;
|
|
isOpen: boolean;
|
|
onClose?: () => void;
|
|
onOpenChange?: (isOpen: boolean) => void;
|
|
title?: string;
|
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
primaryActionLabel?: string;
|
|
onPrimaryAction?: () => void;
|
|
secondaryActionLabel?: string;
|
|
onSecondaryAction?: () => void;
|
|
footer?: ReactNode;
|
|
description?: string;
|
|
icon?: ReactNode;
|
|
}
|
|
|
|
export const Modal = ({
|
|
children,
|
|
isOpen,
|
|
onClose,
|
|
onOpenChange,
|
|
title,
|
|
size = 'md',
|
|
primaryActionLabel,
|
|
onPrimaryAction,
|
|
secondaryActionLabel,
|
|
onSecondaryAction,
|
|
footer,
|
|
description,
|
|
icon
|
|
}: ModalProps) => {
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = 'unset';
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = 'unset';
|
|
};
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const sizeMap = {
|
|
sm: '24rem',
|
|
md: '32rem',
|
|
lg: '48rem',
|
|
xl: '64rem',
|
|
full: '100%',
|
|
};
|
|
|
|
const handleClose = () => {
|
|
if (onClose) onClose();
|
|
if (onOpenChange) onOpenChange(false);
|
|
};
|
|
|
|
return createPortal(
|
|
<Box
|
|
position="fixed"
|
|
inset={0}
|
|
zIndex={100}
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
padding={4}
|
|
bg="rgba(0, 0, 0, 0.8)"
|
|
>
|
|
<Box
|
|
position="absolute"
|
|
inset={0}
|
|
onClick={handleClose}
|
|
/>
|
|
|
|
<Surface
|
|
variant="default"
|
|
rounded="lg"
|
|
shadow="xl"
|
|
style={{
|
|
width: '100%',
|
|
maxWidth: sizeMap[size],
|
|
maxHeight: '90vh',
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
position: 'relative',
|
|
border: '1px solid var(--ui-color-border-default)'
|
|
}}
|
|
>
|
|
<Box
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="between"
|
|
padding={4}
|
|
borderBottom
|
|
>
|
|
<Box display="flex" alignItems="center" gap={3}>
|
|
{icon}
|
|
<Box>
|
|
{title && <Heading level={3}>{title}</Heading>}
|
|
{description && <Box marginTop={1}><Text size="sm" variant="low">{description}</Text></Box>}
|
|
</Box>
|
|
</Box>
|
|
<IconButton icon={X} onClick={handleClose} variant="ghost" title="Close modal" />
|
|
</Box>
|
|
|
|
<Box flex={1} overflow="auto" padding={6}>
|
|
{children}
|
|
</Box>
|
|
|
|
{(footer || primaryActionLabel || secondaryActionLabel) && (
|
|
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)" display="flex" justifyContent="end" gap={3}>
|
|
{footer}
|
|
{secondaryActionLabel && (
|
|
<Button
|
|
onClick={onSecondaryAction || handleClose}
|
|
variant="ghost"
|
|
>
|
|
{secondaryActionLabel}
|
|
</Button>
|
|
)}
|
|
{primaryActionLabel && (
|
|
<Button
|
|
onClick={onPrimaryAction}
|
|
variant="primary"
|
|
>
|
|
{primaryActionLabel}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Surface>
|
|
</Box>,
|
|
document.body
|
|
);
|
|
};
|