'use client'; import { Check, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Progress } from '@/components/ui/progress'; import { cn } from '@/lib/utils'; const SUCCESS_DURATION_MS = 4000; export type DownloadToastState = { title: string; description?: string; /** 0-100 when the total size is known, null while it is unknown. */ percent?: number | null; status?: 'loading' | 'success'; }; export type DownloadProgressToastHandle = { update: (next: Partial) => void; success: (title: string, description?: string) => void; dismiss: () => void; }; // Minimizing is a "get out of my way" choice, so it sticks for the rest of the // session instead of every new download popping the panel open again. let sessionMinimized = false; function minimizedLabel(state: DownloadToastState): string { if (state.status === 'success') return 'Done'; return typeof state.percent === 'number' ? `${Math.round(state.percent)}%` : 'Downloading'; } function DownloadProgressToast({ state, minimized, onToggleMinimized, }: { state: DownloadToastState; minimized: boolean; onToggleMinimized: () => void; }) { const isSuccess = state.status === 'success'; const StatusIcon = isSuccess ? Check : Loader2; return ( // The wrapper spans the toast column but stays click-through, so the // controls underneath (comment composer, voice recording) keep working // wherever the panel itself isn't.
{minimized ? ( ) : (
{!isSuccess && typeof state.percent === 'number' && ( )}
)}
); } /** * Long downloads are streamed through the browser with no native progress UI, * so we show our own — but it sits in the bottom-right corner, on top of the * comment composer. This wraps the progress toast in a panel the user can * collapse to a small pill (and expand again) while the download continues. */ export function createDownloadProgressToast( id: string, initial: DownloadToastState ): DownloadProgressToastHandle { let state: DownloadToastState = { status: 'loading', percent: null, ...initial }; let minimized = sessionMinimized; let duration: number = Number.POSITIVE_INFINITY; const render = () => { toast.custom( () => ( { minimized = !minimized; sessionMinimized = minimized; render(); }} /> ), // The
  • sonner wraps this in is as wide as the toast column even when // only the pill is showing, so clicks pass through it too. { id, duration, className: 'pointer-events-none' } ); }; render(); return { update(next) { state = { ...state, ...next }; render(); }, success(title, description) { state = { ...state, title, description, percent: 100, status: 'success' }; duration = SUCCESS_DURATION_MS; render(); }, dismiss() { toast.dismiss(id); }, }; }