fix(ux): replace confirm() with ConfirmModal, fix dark-mode colors, add currency format
- Add reusable ConfirmModal component (themed, Escape key, focus trap) - Replace all native confirm() calls in Orders, ProductLibrary, Materials, Admin, Billing - Fix ValidationDialog (Upload.tsx) hardcoded bg-white/text-gray-* → semantic tokens - Fix NewInvoiceModal (Billing.tsx) hardcoded colors → semantic tokens - Add formatCurrency (Intl.NumberFormat de-DE/EUR) to NewProductOrder wizard - Resolves audit issues C1, C3, M3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
interface ConfirmModalProps {
|
||||
open: boolean
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
confirmVariant?: 'danger' | 'primary'
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function ConfirmModal({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
confirmLabel = 'Delete',
|
||||
confirmVariant = 'danger',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmModalProps) {
|
||||
const cancelRef = useRef<HTMLButtonElement>(null)
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
// Focus cancel button when modal opens
|
||||
cancelRef.current?.focus()
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
// Trap focus inside the modal
|
||||
if (e.key === 'Tab' && dialogRef.current) {
|
||||
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
)
|
||||
const first = focusable[0]
|
||||
const last = focusable[focusable.length - 1]
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault()
|
||||
last?.focus()
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault()
|
||||
first?.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [open, onCancel])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-modal-title"
|
||||
className="rounded-xl shadow-2xl w-full max-w-sm mx-4 border"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-bg-surface)',
|
||||
borderColor: 'var(--color-border-default)',
|
||||
}}
|
||||
>
|
||||
<div className="px-6 py-5">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="shrink-0 w-9 h-9 rounded-full bg-red-100 flex items-center justify-center">
|
||||
<AlertTriangle size={18} className="text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2
|
||||
id="confirm-modal-title"
|
||||
className="text-base font-semibold text-content"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-sm text-content mt-1" style={{ opacity: 0.7 }}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
ref={cancelRef}
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm rounded-lg border transition-colors text-content-secondary hover:bg-surface-hover"
|
||||
style={{ borderColor: 'var(--color-border-default)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`px-4 py-2 text-sm rounded-lg font-medium transition-colors ${
|
||||
confirmVariant === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700 text-white'
|
||||
: 'bg-accent hover:bg-accent text-white'
|
||||
}`}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user