143 lines
4.0 KiB
TypeScript
143 lines
4.0 KiB
TypeScript
|
|
|
|
import React, {
|
|
type KeyboardEvent as ReactKeyboardEvent,
|
|
type ReactNode,
|
|
} from 'react';
|
|
import { Box } from './Box';
|
|
import { Button } from './Button';
|
|
import { Heading } from './Heading';
|
|
import { Stack } from './Stack';
|
|
import { Text } from './Text';
|
|
|
|
interface ModalProps {
|
|
title: string;
|
|
description?: string;
|
|
icon?: ReactNode;
|
|
children?: ReactNode;
|
|
primaryActionLabel?: string;
|
|
secondaryActionLabel?: string;
|
|
onPrimaryAction?: () => void | Promise<void>;
|
|
onSecondaryAction?: () => void;
|
|
onOpenChange?: (open: boolean) => void;
|
|
isOpen: boolean;
|
|
footer?: ReactNode;
|
|
}
|
|
|
|
export function Modal({
|
|
title,
|
|
description,
|
|
icon,
|
|
children,
|
|
primaryActionLabel,
|
|
secondaryActionLabel,
|
|
onPrimaryAction,
|
|
onSecondaryAction,
|
|
onOpenChange,
|
|
isOpen,
|
|
footer,
|
|
}: ModalProps) {
|
|
const handleKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
if (event.key === 'Escape') {
|
|
if (onOpenChange) {
|
|
onOpenChange(false);
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
|
|
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
if (event.target === event.currentTarget && onOpenChange) {
|
|
onOpenChange(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
style={{ position: 'fixed', inset: 0, zIndex: 60, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: '0 1rem', backdropFilter: 'blur(4px)' }}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title"
|
|
aria-describedby={description ? 'modal-description' : undefined}
|
|
onKeyDown={handleKeyDown}
|
|
onClick={handleBackdropClick}
|
|
>
|
|
<Box
|
|
style={{ width: '100%', maxWidth: '28rem', borderRadius: '1rem', backgroundColor: '#0f1115', border: '1px solid #262626', boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)', outline: 'none', overflow: 'hidden' }}
|
|
tabIndex={-1}
|
|
>
|
|
<Box p={6} style={{ borderBottom: '1px solid rgba(38, 38, 38, 0.8)' }}>
|
|
<Stack direction="row" align="center" gap={3}>
|
|
{icon && <Box>{icon}</Box>}
|
|
<Box>
|
|
<Heading level={2} id="modal-title">{title}</Heading>
|
|
{description && (
|
|
<Text
|
|
id="modal-description"
|
|
size="sm"
|
|
color="text-gray-400"
|
|
block
|
|
mt={1}
|
|
>
|
|
{description}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
|
|
<Box p={6}>
|
|
{children}
|
|
</Box>
|
|
|
|
{(primaryActionLabel || secondaryActionLabel || footer) && (
|
|
<Box p={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.8)' }}>
|
|
{(primaryActionLabel || secondaryActionLabel) && (
|
|
<Box style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
|
|
{secondaryActionLabel && (
|
|
<Button
|
|
type="button"
|
|
onClick={() => {
|
|
onSecondaryAction?.();
|
|
onOpenChange?.(false);
|
|
}}
|
|
variant="secondary"
|
|
size="sm"
|
|
fullWidth={!primaryActionLabel}
|
|
>
|
|
{secondaryActionLabel}
|
|
</Button>
|
|
)}
|
|
{primaryActionLabel && (
|
|
<Button
|
|
type="button"
|
|
onClick={async () => {
|
|
if (onPrimaryAction) {
|
|
await onPrimaryAction();
|
|
}
|
|
}}
|
|
variant="primary"
|
|
size="sm"
|
|
fullWidth={!secondaryActionLabel}
|
|
>
|
|
{primaryActionLabel}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
)}
|
|
{footer && (
|
|
<Box mt={4}>
|
|
{footer}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|