Files
gridpilot.gg/apps/website/ui/Modal.tsx
2026-01-18 17:55:04 +01:00

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>
);
}