toast.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. 'use client';
  2. import * as React from 'react';
  3. import { Toast as ToastPrimitive } from '@base-ui/react/toast';
  4. import { cn } from '@/lib/utils';
  5. import { Button } from '@/components/ui/button';
  6. import {
  7. XIcon,
  8. CircleCheckIcon,
  9. InfoIcon,
  10. TriangleAlertIcon,
  11. OctagonXIcon,
  12. Loader2Icon,
  13. } from 'lucide-react';
  14. const toast = ToastPrimitive.createToastManager();
  15. function ToastProvider({ ...props }: ToastPrimitive.Provider.Props) {
  16. return <ToastPrimitive.Provider {...props} />;
  17. }
  18. function ToastPortal({ ...props }: ToastPrimitive.Portal.Props) {
  19. return <ToastPrimitive.Portal data-slot="toast-portal" {...props} />;
  20. }
  21. function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
  22. return (
  23. <ToastPrimitive.Viewport
  24. data-slot="toast-viewport"
  25. className={cn(
  26. 'pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full',
  27. className,
  28. )}
  29. {...props}
  30. />
  31. );
  32. }
  33. function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
  34. return (
  35. <ToastPrimitive.Root
  36. data-slot="toast"
  37. className={cn(
  38. 'rounded-2xl group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
  39. '[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]',
  40. 'h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]',
  41. "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
  42. 'data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]',
  43. 'data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]',
  44. '[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]',
  45. 'data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
  46. 'data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
  47. 'data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
  48. 'data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
  49. 'data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
  50. 'data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
  51. 'data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
  52. 'data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
  53. className,
  54. )}
  55. {...props}
  56. />
  57. );
  58. }
  59. function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
  60. return (
  61. <ToastPrimitive.Content
  62. data-slot="toast-content"
  63. className={cn(
  64. 'flex h-full items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100',
  65. className,
  66. )}
  67. {...props}
  68. />
  69. );
  70. }
  71. function ToastTitle({ className, ...props }: ToastPrimitive.Title.Props) {
  72. return (
  73. <ToastPrimitive.Title
  74. data-slot="toast-title"
  75. className={cn('text-sm font-medium', className)}
  76. {...props}
  77. />
  78. );
  79. }
  80. function ToastDescription({
  81. className,
  82. ...props
  83. }: ToastPrimitive.Description.Props) {
  84. return (
  85. <ToastPrimitive.Description
  86. data-slot="toast-description"
  87. className={cn('text-sm text-muted-foreground', className)}
  88. {...props}
  89. />
  90. );
  91. }
  92. function ToastAction({
  93. className,
  94. render = <Button variant="outline" size="sm" />,
  95. ...props
  96. }: ToastPrimitive.Action.Props) {
  97. return (
  98. <ToastPrimitive.Action
  99. data-slot="toast-action"
  100. render={render}
  101. className={cn('shrink-0', className)}
  102. {...props}
  103. />
  104. );
  105. }
  106. function ToastClose({
  107. className,
  108. children,
  109. render = <Button variant="ghost" size="icon-sm" />,
  110. ...props
  111. }: ToastPrimitive.Close.Props) {
  112. return (
  113. <ToastPrimitive.Close
  114. data-slot="toast-close"
  115. aria-label="Close toast"
  116. render={render}
  117. className={cn(
  118. "relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
  119. className,
  120. )}
  121. {...props}
  122. >
  123. {children ?? <XIcon aria-hidden="true" />}
  124. </ToastPrimitive.Close>
  125. );
  126. }
  127. function ToastIcon({ type }: { type: string | undefined }) {
  128. let icon: React.ReactNode = null;
  129. if (type === 'success') {
  130. icon = <CircleCheckIcon aria-hidden="true" />;
  131. }
  132. if (type === 'info') {
  133. icon = <InfoIcon aria-hidden="true" />;
  134. }
  135. if (type === 'warning') {
  136. icon = <TriangleAlertIcon aria-hidden="true" />;
  137. }
  138. if (type === 'error') {
  139. icon = <OctagonXIcon className="text-destructive" aria-hidden="true" />;
  140. }
  141. if (type === 'loading') {
  142. icon = <Loader2Icon className="animate-spin" aria-hidden="true" />;
  143. }
  144. if (!icon) {
  145. return null;
  146. }
  147. return (
  148. <span
  149. data-slot="toast-icon"
  150. className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
  151. >
  152. {icon}
  153. </span>
  154. );
  155. }
  156. function ToastList() {
  157. const { toasts } = ToastPrimitive.useToastManager();
  158. return toasts.map((toastItem) => (
  159. <Toast key={toastItem.id} toast={toastItem}>
  160. <ToastContent>
  161. <ToastIcon type={toastItem.type} />
  162. <div className="flex min-w-0 flex-1 flex-col gap-1">
  163. <ToastTitle />
  164. <ToastDescription />
  165. </div>
  166. <ToastAction />
  167. <ToastClose />
  168. </ToastContent>
  169. </Toast>
  170. ));
  171. }
  172. function Toaster({
  173. children,
  174. toastManager = toast,
  175. ...props
  176. }: ToastPrimitive.Provider.Props) {
  177. return (
  178. <ToastProvider toastManager={toastManager} {...props}>
  179. {children}
  180. <ToastPortal>
  181. <ToastViewport>
  182. <ToastList />
  183. </ToastViewport>
  184. </ToastPortal>
  185. </ToastProvider>
  186. );
  187. }
  188. const createToastManager = ToastPrimitive.createToastManager;
  189. const useToastManager = ToastPrimitive.useToastManager;
  190. export {
  191. Toaster,
  192. Toast,
  193. ToastAction,
  194. ToastClose,
  195. ToastContent,
  196. ToastDescription,
  197. ToastPortal,
  198. ToastProvider,
  199. ToastTitle,
  200. ToastViewport,
  201. createToastManager,
  202. toast,
  203. useToastManager,
  204. };