undo / redo behaviour, workspace concept
This commit is contained in:
193
src/components/ActionDialog.tsx
Normal file
193
src/components/ActionDialog.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export interface ActionDialogAction {
|
||||
label: string;
|
||||
onClick: () => void | Promise<void>;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ActionDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
actions: ActionDialogAction[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const backgroundByVariant: Record<
|
||||
NonNullable<ActionDialogAction['variant']>,
|
||||
string
|
||||
> = {
|
||||
primary: '#2563eb',
|
||||
secondary: '#e5e7eb',
|
||||
danger: '#dc2626',
|
||||
};
|
||||
|
||||
const colorByVariant: Record<
|
||||
NonNullable<ActionDialogAction['variant']>,
|
||||
string
|
||||
> = {
|
||||
primary: 'white',
|
||||
secondary: '#111827',
|
||||
danger: 'white',
|
||||
};
|
||||
|
||||
const ActionDialog: React.FC<ActionDialogProps> = ({
|
||||
open,
|
||||
title,
|
||||
children,
|
||||
actions,
|
||||
onClose,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="action-dialog-title"
|
||||
onPointerDown={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 70,
|
||||
background: 'rgba(15, 23, 42, 0.55)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '440px',
|
||||
background: 'white',
|
||||
borderRadius: '0.75rem',
|
||||
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.35)',
|
||||
padding: '1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
id="action-dialog-title"
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: '1rem',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '999px',
|
||||
width: '1.8rem',
|
||||
height: '1.8rem',
|
||||
background: '#e5e7eb',
|
||||
color: '#111827',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.1rem',
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.9rem',
|
||||
color: '#4b5563',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: '0.25rem',
|
||||
}}
|
||||
>
|
||||
{actions.map((action) => {
|
||||
const variant = action.variant ?? 'secondary';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={action.label}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void action.onClick();
|
||||
}}
|
||||
disabled={action.disabled}
|
||||
autoFocus={action.autoFocus}
|
||||
title={action.title}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '0.45rem 0.8rem',
|
||||
background: action.disabled
|
||||
? '#e5e7eb'
|
||||
: backgroundByVariant[variant],
|
||||
color: action.disabled ? '#6b7280' : colorByVariant[variant],
|
||||
cursor: action.disabled ? 'default' : 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionDialog;
|
||||
Reference in New Issue
Block a user