Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 10x | import { cn } from '@sb/libs';
import { motion } from 'framer-motion';
import { ToastProps } from '../Toast.types';
import { useEffect, useRef, useState } from 'react';
import { X } from '@sb/ui/components/atoms/Icons/web/Icons';
import { useIsMobile } from '@sb/hooks/Utilities/useIsMobile';
import { TOAST_VARIANTS as variants, secondsToMillis } from '../utils';
export const Toast = ({ title, message, variant, duration = 'short', onClose }: ToastProps) => {
const isMobile = useIsMobile();
const [progress, setProgress] = useState(0);
const [paused, setPaused] = useState(false);
const startTimeRef = useRef<number | null>(null);
const durationMs = duration === 'short' ? secondsToMillis(3) : secondsToMillis(6);
const { backgroundColor, toastContentColor, iconBackground, icon } = variants[variant];
useEffect(() => {
let frame: number;
const tick = (timestamp: number) => {
if (!startTimeRef.current) startTimeRef.current = timestamp;
const elapsed = timestamp - startTimeRef.current;
const percent = Math.min((elapsed / durationMs) * 100, 100);
setProgress(percent);
if (percent < 100) {
frame = requestAnimationFrame(tick);
} else {
onClose?.();
}
};
if (!paused) {
frame = requestAnimationFrame(tick);
}
return () => cancelAnimationFrame(frame);
}, [paused, durationMs, onClose]);
const handleMouseEnter = () => setPaused(true);
const handleMouseLeave = () => {
setPaused(false);
startTimeRef.current = performance.now() - (progress / 100) * durationMs;
};
const toastColors = {
iconBackground,
backgroundColor,
textColor: toastContentColor,
closeIconColor: toastContentColor,
progressIndicatorColor: toastContentColor,
closeIconBorderColor: toastContentColor,
};
return (
<motion.div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
transition={{ type: 'spring', stiffness: 300, damping: 24 }}
style={{
color: toastColors.textColor,
backgroundColor: toastColors.backgroundColor,
}}
className={cn(
'min-w-70 relative flex w-full max-w-80 items-center overflow-hidden rounded-xl shadow-xl',
{
'bottom-14 right-0': isMobile,
}
)}
>
<div className="flex flex-1 flex-row items-center justify-between gap-2 p-3">
<div
style={{ backgroundColor: iconBackground }}
className="grid size-8 place-items-center rounded-full"
>
{icon}
</div>
<div className="pointer-events-none flex-1">
{title && <p className="font-semibold">{title}</p>}
<p className="text-sm">{message}</p>
</div>
<div
role="button"
onClick={onClose}
style={{
borderColor: toastColors.closeIconBorderColor,
}}
className={`cursor-pointer rounded-full border p-2 opacity-50 hover:opacity-100`}
>
<X color={toastColors.closeIconColor} />
</div>
</div>
{/* Duration progress bar */}
<motion.div
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={progress}
initial={{ width: '0%' }}
animate={{ width: `${progress}%` }}
transition={{ ease: 'linear', duration: 0.1 }}
className={`absolute bottom-0 left-0 h-[3px] opacity-80`}
style={{ backgroundColor: toastColors.progressIndicatorColor }}
/>
</motion.div>
);
};
|