Phase 10 Implementation: Bulk color system compliance across frontend Applied DESIGN_COLOR_RULES.md to 40 component and page files: - Indigo (3) → primary/secondary (primary actions) - Green (12) → tertiary (#00e639) (success/healthy status) - Red/Rose (20) → error (#ffb4ab) (destructive actions) - Amber/Sky (9) → primary/secondary (warnings/info) - Blue/Slate (various) → primary/design system colors (focus states) - Black → bg-surface-container-lowest (proper design color) Files updated: 40 components + pages Color replacements: 44+ instances Build status: ✓ Zero errors, compiled in 6.3s Test status: Pending (npm run test) All colors now follow DESIGN.md Industrial Precision system: ✅ Primary: #ffb781 (caution orange) ✅ Secondary: #c8c6c5 (gray) ✅ Tertiary: #00e639 (healthy green) ✅ Error: #ffb4ab (destructive red) ✅ Design system enforced across all UI/UX Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { AlertTriangle, Loader2, X } from 'lucide-react';
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
export interface ConfirmationModalProps {
|
|
show: boolean;
|
|
title: string; // "Delete User: alex_smith?"
|
|
description: string; // "This will remove the user from the system."
|
|
itemName?: string; // The item being deleted (for display)
|
|
affectedCount?: number; // Number of affected items (e.g., 47)
|
|
consequence?: string; // "This cannot be undone." or "Audit logs will remain."
|
|
onConfirm: () => Promise<void>;
|
|
onCancel: () => void;
|
|
loading?: boolean;
|
|
dangerLevel?: 'low' | 'medium' | 'high'; // Determines if confirmation text is required
|
|
}
|
|
|
|
export default function ConfirmationModal({
|
|
show,
|
|
title,
|
|
description,
|
|
itemName,
|
|
affectedCount,
|
|
consequence,
|
|
onConfirm,
|
|
onCancel,
|
|
loading = false,
|
|
dangerLevel = 'medium'
|
|
}: ConfirmationModalProps) {
|
|
const [confirmText, setConfirmText] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (!show) return null;
|
|
|
|
const isHighRisk = dangerLevel === 'high';
|
|
const requiresTextConfirm = isHighRisk && affectedCount && affectedCount > 10;
|
|
const canConfirm = !requiresTextConfirm || confirmText === 'Delete';
|
|
|
|
const handleConfirm = async () => {
|
|
if (requiresTextConfirm && confirmText !== 'Delete') {
|
|
setError('Type "Delete" to confirm this action');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await onConfirm();
|
|
setConfirmText('');
|
|
setError(null);
|
|
} catch (err: any) {
|
|
setError(err?.message || 'Failed to delete item');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div data-testid="confirmation-modal" className="fixed inset-0 z-50 flex items-center justify-center bg-surface-container-lowest/40">
|
|
<div className="bg-surface border border-border shadow-none max-w-sm w-full mx-4">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-6 border-b border-border">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-error/10 text-error">
|
|
<AlertTriangle size={20} />
|
|
</div>
|
|
<h2 className="text-lg font-normal text-white">{title}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onCancel}
|
|
disabled={loading}
|
|
className="p-1 text-muted hover:text-secondary cursor-pointer focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
|
aria-label="Close modal"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 space-y-4">
|
|
{/* Description */}
|
|
<p className="text-sm text-secondary">{description}</p>
|
|
|
|
{/* Item Name (if provided) */}
|
|
{itemName && (
|
|
<div className="bg-surface-container-lowest/40 border border-border p-3">
|
|
<p className="text-xs text-muted font-normal">Item</p>
|
|
<p className="text-sm font-mono text-white mt-1">{itemName}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Consequence Warning */}
|
|
{consequence && (
|
|
<div className="flex gap-2 p-3 bg-error/5 border border-error/20">
|
|
<span className="text-error flex-shrink-0">⚠</span>
|
|
<p className="text-sm text-error">{consequence}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Affected Items Count (for medium-risk+) */}
|
|
{affectedCount && affectedCount > 0 && (
|
|
<div className="text-sm text-secondary">
|
|
{affectedCount === 1
|
|
? 'This will affect 1 item.'
|
|
: `This will affect ${affectedCount} items.`}
|
|
</div>
|
|
)}
|
|
|
|
{/* High-Risk Warning & Confirmation Input */}
|
|
{requiresTextConfirm && (
|
|
<div className="space-y-3 pt-2 border-t border-border">
|
|
<p className="text-sm font-normal text-error">
|
|
Type "Delete" to confirm this high-risk action
|
|
</p>
|
|
<input
|
|
type="text"
|
|
value={confirmText}
|
|
onChange={(e) => {
|
|
setConfirmText(e.target.value);
|
|
setError(null);
|
|
}}
|
|
placeholder="Type Delete"
|
|
className="w-full"
|
|
disabled={loading}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && canConfirm && !loading) {
|
|
handleConfirm();
|
|
}
|
|
}}
|
|
/>
|
|
{error && (
|
|
<p className="text-xs text-error">{error}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-2 p-6 border-t border-border">
|
|
<button
|
|
onClick={onCancel}
|
|
disabled={loading}
|
|
className="btn-secondary flex-1"
|
|
>
|
|
Keep Item
|
|
</button>
|
|
<button
|
|
onClick={handleConfirm}
|
|
data-testid="confirm-action"
|
|
disabled={loading || !canConfirm}
|
|
className="flex-1 px-4 py-2.5 bg-error text-white font-normal hover:hover:opacity-80 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 focus:outline-none"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 size={16} className="animate-spin" />
|
|
Deleting...
|
|
</>
|
|
) : (
|
|
'Delete'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|