Files
gridpilot.gg/apps/website/ui/Modal.tsx
2026-01-14 23:46:04 +01:00

122 lines
3.3 KiB
TypeScript

'use client';
import React, {
type ReactNode,
type KeyboardEvent as ReactKeyboardEvent,
} from 'react';
import { Box } from './Box';
import { Text } from './Text';
import { Heading } from './Heading';
import { Button } from './Button';
interface ModalProps {
title: string;
description?: string;
children?: ReactNode;
primaryActionLabel?: string;
secondaryActionLabel?: string;
onPrimaryAction?: () => void | Promise<void>;
onSecondaryAction?: () => void;
onOpenChange?: (open: boolean) => void;
isOpen: boolean;
}
export function Modal({
title,
description,
children,
primaryActionLabel,
secondaryActionLabel,
onPrimaryAction,
onSecondaryAction,
onOpenChange,
isOpen,
}: 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' }}
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' }}
tabIndex={-1}
>
<Box p={6} style={{ borderBottom: '1px solid rgba(38, 38, 38, 0.8)' }}>
<Heading level={2} id="modal-title">{title}</Heading>
{description && (
<Text
id="modal-description"
size="sm"
color="text-gray-400"
block
mt={2}
>
{description}
</Text>
)}
</Box>
<Box p={6}>
<Text size="sm" color="text-gray-100">{children}</Text>
</Box>
{(primaryActionLabel || secondaryActionLabel) && (
<Box p={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.8)', display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
{secondaryActionLabel && (
<Button
type="button"
onClick={() => {
onSecondaryAction?.();
onOpenChange?.(false);
}}
variant="secondary"
size="sm"
>
{secondaryActionLabel}
</Button>
)}
{primaryActionLabel && (
<Button
type="button"
onClick={async () => {
if (onPrimaryAction) {
await onPrimaryAction();
}
}}
variant="primary"
size="sm"
>
{primaryActionLabel}
</Button>
)}
</Box>
)}
</Box>
</Box>
);
}