import React, { forwardRef, useEffect, useRef, useState } from 'react'; import { Tone, Surface, cn, toneMap, surfaceClasses, useEffectiveSurface, CloseIcon, } from '../common'; // Type-only import (erased at runtime): PxlKitToastProvider.tsx value-imports // PixelToast back, so keeping this edge type-only avoids a runtime cycle. import type { ToastItem } from './PxlKitToastProvider'; /* ────────────────────────────────────────────────────────────────────────── PixelToast — individual toast card. ────────────────────────────────────────────────────────────────────────── */ /** Public prop bag for the individual {@link PixelToast} card. Usually used * through {@link useToast}, but exported for advanced custom rendering. */ export interface PixelToastProps { toast: ToastItem; onDismiss: () => void; surface?: Surface; } /** Tiny inline spinner used when `toast.loading === true`. Keeps PixelToast * self-contained (no cross-module import from feedback.tsx). */ function ToastSpinner({ tone }: { tone: Tone }) { return ( ); } export const PixelToast = forwardRef(function PixelToast( { toast, onDismiss, surface: surfaceProp }, ref, ) { const surface = useEffectiveSurface(surfaceProp); const s = surfaceClasses(surface); const tone: Tone = toast.tone ?? 'cyan'; const duration = toast.loading ? 0 : (toast.duration ?? 4500); const assertive = toast.assertive ?? (tone === 'red' || tone === 'gold'); const [paused, setPaused] = useState(false); const start = useRef(0); const remaining = useRef(duration); // Keep the latest onDismiss in a ref so re-rendered ToastViewport children // (e.g. another toast pushed mid-duration) don't reset the auto-dismiss timer. const onDismissRef = useRef(onDismiss); useEffect(() => { onDismissRef.current = onDismiss; }, [onDismiss]); // Reset the timer whenever the underlying toast changes its duration // (e.g. promise resolved → loading→success patch flips duration 0→4500). useEffect(() => { remaining.current = duration; }, [duration]); useEffect(() => { if (!duration) return; let timeout: ReturnType | null = null; if (!paused) { start.current = Date.now(); timeout = setTimeout(() => onDismissRef.current(), remaining.current); } return () => { if (timeout) clearTimeout(timeout); }; }, [duration, paused]); const onMouseEnter = () => { if (!duration) return; remaining.current = Math.max(0, remaining.current - (Date.now() - start.current)); setPaused(true); }; const onMouseLeave = () => { if (!duration) return; setPaused(false); }; const leadingIcon = toast.animatedIcon ?? (toast.loading ? : toast.icon); return ( {surface === 'pixel' && ( )} {leadingIcon && ( {leadingIcon} )} {toast.title} {toast.message && {toast.message}} {toast.action && {toast.action}} {duration > 0 && ( )} ); });
{toast.title}
{toast.message}