- Created automated script (scripts/replace-decorative-cyan.py) to systematically replace decorative/informational kodo-cyan instances with kodo-steel variants - Script intelligently preserves active/functional states, design system variants, semantic indicators, and interactive states - Modified 85 files, replaced 145 decorative instances, preserved 47 functional instances - No linter errors, type safety maintained - Action 11.3.1.3 significantly advanced (total: ~302 instances replaced across ~229 files including previous batches)
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react';
|
|
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export interface Toast {
|
|
id: string;
|
|
message: string;
|
|
type?: 'success' | 'error' | 'warning' | 'info';
|
|
duration?: number;
|
|
}
|
|
|
|
export interface ToastProps {
|
|
toast: Toast;
|
|
onDismiss: (id: string) => void;
|
|
}
|
|
|
|
const TOAST_ICONS = {
|
|
success: CheckCircle,
|
|
error: AlertCircle,
|
|
warning: AlertTriangle,
|
|
info: Info,
|
|
};
|
|
|
|
const TOAST_STYLES = {
|
|
success:
|
|
'bg-kodo-lime/10 border-kodo-lime/30 text-kodo-text-main dark:bg-kodo-lime/20 dark:border-kodo-lime/40 dark:text-kodo-lime',
|
|
error:
|
|
'bg-kodo-red/10 border-kodo-red/30 text-kodo-text-main dark:bg-kodo-red/20 dark:border-kodo-red/40 dark:text-kodo-red',
|
|
warning:
|
|
'bg-kodo-gold/10 border-kodo-gold/30 text-kodo-text-main dark:bg-kodo-gold/20 dark:border-kodo-gold/40 dark:text-kodo-gold',
|
|
info: 'bg-kodo-steel/10 border-kodo-steel/30 text-kodo-text-main dark:bg-kodo-steel/20 dark:border-kodo-steel/40 dark:text-kodo-steel',
|
|
};
|
|
|
|
const DEFAULT_DURATION = 5000;
|
|
|
|
/**
|
|
* Composant Toast individuel pour afficher une notification.
|
|
*/
|
|
export function ToastComponent({ toast, onDismiss }: ToastProps) {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const [isLeaving, setIsLeaving] = useState(false);
|
|
|
|
const handleDismiss = useCallback(() => {
|
|
setIsLeaving(true);
|
|
setTimeout(() => {
|
|
onDismiss(toast.id);
|
|
}, 300);
|
|
}, [toast.id, onDismiss]);
|
|
|
|
useEffect(() => {
|
|
// Animation d'entrée - rendre visible immédiatement
|
|
setIsVisible(true);
|
|
|
|
// Auto-dismiss
|
|
const duration = toast.duration ?? DEFAULT_DURATION;
|
|
let dismissTimer: NodeJS.Timeout | null = null;
|
|
if (duration > 0) {
|
|
dismissTimer = setTimeout(() => {
|
|
handleDismiss();
|
|
}, duration);
|
|
}
|
|
|
|
return () => {
|
|
if (dismissTimer) clearTimeout(dismissTimer);
|
|
};
|
|
}, [toast.duration, toast.id, handleDismiss]);
|
|
|
|
const Icon = toast.type ? TOAST_ICONS[toast.type] : Info;
|
|
const styles = toast.type ? TOAST_STYLES[toast.type] : TOAST_STYLES.info;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'relative flex min-w-[300px] max-w-md items-start gap-3 rounded-lg border p-4 shadow-lg transition-all duration-300',
|
|
styles,
|
|
isVisible && !isLeaving
|
|
? 'opacity-100 translate-x-0'
|
|
: 'opacity-0 translate-x-full',
|
|
)}
|
|
role="alert"
|
|
aria-live="polite"
|
|
>
|
|
<Icon className="h-5 w-5 flex-shrink-0" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{toast.message}</p>
|
|
</div>
|
|
<button
|
|
onClick={handleDismiss}
|
|
className="flex-shrink-0 rounded-md p-1 text-current opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
aria-label="Fermer"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|