learning_ai_common_plat/packages/ui/src/components/Toast.tsx

98 lines
2.8 KiB
TypeScript

'use client';
import * as React from 'react';
import { clsx } from 'clsx';
import { X, CheckCircle, AlertTriangle, Info, AlertCircle } from 'lucide-react';
export interface ToastMessage {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title: string;
description?: string;
duration?: number;
}
type ToastListener = (toasts: ToastMessage[]) => void;
let globalToasts: ToastMessage[] = [];
const listeners = new Set<ToastListener>();
function notifyListeners() {
listeners.forEach(l => l([...globalToasts]));
}
export function toast(msg: Omit<ToastMessage, 'id'>) {
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
globalToasts = [...globalToasts, { ...msg, id }];
notifyListeners();
const duration = msg.duration ?? 5000;
if (duration > 0) setTimeout(() => dismissToast(id), duration);
}
export function dismissToast(id: string) {
globalToasts = globalToasts.filter(t => t.id !== id);
notifyListeners();
}
export function useToast() {
return { toast, dismiss: dismissToast };
}
const icons: Record<string, React.ReactNode> = {
success: <CheckCircle className="h-5 w-5 text-green-400" />,
error: <AlertCircle className="h-5 w-5 text-red-400" />,
warning: <AlertTriangle className="h-5 w-5 text-yellow-400" />,
info: <Info className="h-5 w-5 text-blue-400" />,
};
export function Toast({ message, onDismiss }: { message: ToastMessage; onDismiss: () => void }) {
return (
<div
role="alert"
className={clsx(
'flex items-start gap-3 rounded-lg border p-4 shadow-lg backdrop-blur-sm',
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]'
)}
>
{icons[message.type]}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{message.title}</p>
{message.description && (
<p className="mt-1 text-xs text-[var(--bl-text-secondary,#a0a0b0)]">
{message.description}
</p>
)}
</div>
<button
onClick={onDismiss}
className="text-[var(--bl-text-tertiary,#666)] hover:text-[var(--bl-text-primary,#fff)]"
aria-label="Dismiss notification"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastMessage[]>([]);
React.useEffect(() => {
listeners.add(setToasts);
return () => {
listeners.delete(setToasts);
};
}, []);
return (
<>
{children}
<div
className="fixed bottom-4 right-4 z-[9999] flex flex-col gap-2 max-w-sm"
aria-live="polite"
>
{toasts.map(t => (
<Toast key={t.id} message={t} onDismiss={() => dismissToast(t.id)} />
))}
</div>
</>
);
}