Files
gridpilot.gg/apps/website/ui/Modal.tsx
2026-01-18 23:24:30 +01:00

152 lines
3.7 KiB
TypeScript

import { X } from 'lucide-react';
import { ReactNode, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Box } from './Box';
import { Button } from './Button';
import { Heading } from './Heading';
import { IconButton } from './IconButton';
import { Surface } from './Surface';
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;
actions?: ReactNode;
}
export const Modal = ({
children,
isOpen,
onClose,
onOpenChange,
title,
size = 'md',
primaryActionLabel,
onPrimaryAction,
secondaryActionLabel,
onSecondaryAction,
footer,
description,
icon,
actions
}: 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>
<Box display="flex" alignItems="center" gap={2}>
{actions}
<IconButton icon={X} onClick={handleClose} variant="ghost" title="Close modal" />
</Box>
</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
);
};