This commit is contained in:
cruldra committed 2026-06-23 05:02:15 +08:00
commit e6f1776d4f
264 files changed
+54215

No files matched your search

@@ -0,0 +1,100 @@
import { useEffect } from 'react'
import { CheckCircle2, Loader2, X, XCircle } from 'lucide-react'
export interface ToastItem {
id: string
level: 'info' | 'success' | 'error'
message: string
spinner?: boolean
link?: { label: string, url: string }
dismissOnTimer?: number
}
interface Props {
toasts: ToastItem[]
onDismiss: (id: string) => void
onOpenUrl: (url: string) => void
}
function levelClasses(level: ToastItem['level']): string {
switch (level) {
case 'success':
return 'border-state-green/60 bg-state-green/10'
case 'error':
return 'border-state-red/60 bg-state-red/10'
default:
return 'border-state-blue/60 bg-state-blue/10'
}
}
function LevelIcon({ toast }: { toast: ToastItem }) {
if (toast.spinner)
return <Loader2 className="size-3.5 shrink-0 animate-spin" />
if (toast.level === 'success')
return <CheckCircle2 className="size-3.5 shrink-0 text-state-green" />
if (toast.level === 'error')
return <XCircle className="size-3.5 shrink-0 text-state-red" />
return null
}
function ToastRow({
toast,
onDismiss,
onOpenUrl,
}: {
toast: ToastItem
onDismiss: (id: string) => void
onOpenUrl: (url: string) => void
}) {
useEffect(() => {
if (!toast.dismissOnTimer)
return
const t = setTimeout(() => onDismiss(toast.id), toast.dismissOnTimer)
return () => clearTimeout(t)
}, [toast.id, toast.dismissOnTimer, onDismiss])
return (
<div
className={`flex min-w-[260px] max-w-sm items-start gap-2 rounded border px-3 py-2 text-xs shadow-md ${levelClasses(toast.level)}`}
>
<LevelIcon toast={toast} />
<div className="flex-1 break-words text-[var(--vscode-foreground)] opacity-90">
{toast.message}
{toast.link && (
<>
{' '}
<button
type="button"
onClick={() => onOpenUrl(toast.link!.url)}
className="text-[var(--vscode-textLink-foreground)] underline"
>
{toast.link.label}
</button>
</>
)}
</div>
<button
type="button"
onClick={() => onDismiss(toast.id)}
className="shrink-0 opacity-60 hover:opacity-100"
aria-label="关闭"
>
<X className="size-3.5" />
</button>
</div>
)
}
export function ToastStack({ toasts, onDismiss, onOpenUrl }: Props) {
if (toasts.length === 0)
return null
return (
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex flex-col gap-2">
{toasts.map(t => (
<div key={t.id} className="pointer-events-auto">
<ToastRow toast={t} onDismiss={onDismiss} onOpenUrl={onOpenUrl} />
</div>
))}
</div>
)
}