feat: add ConfirmDialog composite component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-18 23:06:38 +01:00
parent f9addff5a6
commit f7d30c1257
2 changed files with 155 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
.content {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 0;
font-family: var(--font-body);
}
.title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
line-height: 1.3;
}
.message {
font-size: 14px;
color: var(--text-secondary);
margin: 0;
line-height: 1.5;
}
.inputGroup {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 12px;
color: var(--text-secondary);
}
.input {
width: 100%;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-raised);
color: var(--text-primary);
font-family: var(--font-body);
font-size: 12px;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.input:focus {
border-color: var(--amber);
box-shadow: 0 0 0 3px var(--amber-bg);
}
.buttonRow {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 4px;
}

View File

@@ -0,0 +1,97 @@
import { useState, useEffect, useRef } from 'react'
import { Modal } from '../Modal/Modal'
import { Button } from '../../primitives/Button/Button'
import styles from './ConfirmDialog.module.css'
export interface ConfirmDialogProps {
open: boolean
onClose: () => void
onConfirm: () => void
title?: string
message: string
confirmText: string
confirmLabel?: string
cancelLabel?: string
variant?: 'danger' | 'warning' | 'info'
loading?: boolean
className?: string
}
export function ConfirmDialog({
open,
onClose,
onConfirm,
title = 'Confirm Deletion',
message,
confirmText,
confirmLabel = 'Delete',
cancelLabel = 'Cancel',
variant = 'danger',
loading = false,
className,
}: ConfirmDialogProps) {
const [input, setInput] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const matches = input === confirmText
useEffect(() => {
if (open) {
setInput('')
const id = setTimeout(() => inputRef.current?.focus(), 0)
return () => clearTimeout(id)
}
}, [open])
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter' && matches && !loading) {
e.preventDefault()
onConfirm()
}
}
const confirmButtonVariant = variant === 'danger' ? 'danger' : 'primary'
return (
<Modal open={open} onClose={onClose} size="sm" className={className}>
<div className={styles.content}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.message}>{message}</p>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="confirm-input">
{`Type "${confirmText}" to confirm`}
</label>
<input
ref={inputRef}
id="confirm-input"
className={styles.input}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
autoComplete="off"
/>
</div>
<div className={styles.buttonRow}>
<Button
variant="secondary"
onClick={onClose}
disabled={loading}
type="button"
>
{cancelLabel}
</Button>
<Button
variant={confirmButtonVariant}
onClick={onConfirm}
loading={loading}
disabled={!matches || loading}
type="button"
>
{confirmLabel}
</Button>
</div>
</div>
</Modal>
)
}