1- import { ExclamationCircleIcon , XMarkIcon } from "@heroicons/react/20/solid" ;
1+ import { EnvelopeIcon , ExclamationCircleIcon , XMarkIcon } from "@heroicons/react/20/solid" ;
22import { CheckCircleIcon } from "@heroicons/react/24/solid" ;
33import { Toaster , toast } from "sonner" ;
4-
54import { useTypedLoaderData } from "remix-typedjson" ;
6- import { loader } from "~/root" ;
5+ import { type loader } from "~/root" ;
76import { useEffect } from "react" ;
87import { Paragraph } from "./Paragraph" ;
98import { cn } from "~/utils/cn" ;
9+ import { type ToastMessageAction } from "~/models/message.server" ;
10+ import { Header2 , Header3 } from "./Headers" ;
11+ import { Button , LinkButton } from "./Buttons" ;
12+ import { Feedback } from "../Feedback" ;
13+ import assertNever from "assert-never" ;
14+ import { assertExhaustive } from "@trigger.dev/core" ;
1015
1116const defaultToastDuration = 5000 ;
1217const permanentToastDuration = 60 * 60 * 24 * 1000 ;
@@ -19,9 +24,22 @@ export function Toast() {
1924 }
2025 const { message, type, options } = toastMessage ;
2126
22- toast . custom ( ( t ) => < ToastUI variant = { type } message = { message } t = { t as string } /> , {
23- duration : options . ephemeral ? defaultToastDuration : permanentToastDuration ,
24- } ) ;
27+ const ephemeral = options . action ? false : options . ephemeral ;
28+
29+ toast . custom (
30+ ( t ) => (
31+ < ToastUI
32+ variant = { type }
33+ message = { message }
34+ t = { t as string }
35+ title = { options . title }
36+ action = { options . action }
37+ />
38+ ) ,
39+ {
40+ duration : ephemeral ? defaultToastDuration : permanentToastDuration ,
41+ }
42+ ) ;
2543 } , [ toastMessage ] ) ;
2644
2745 return < Toaster /> ;
@@ -32,11 +50,15 @@ export function ToastUI({
3250 message,
3351 t,
3452 toastWidth = 356 , // Default width, matches what sonner provides by default
53+ title,
54+ action,
3555} : {
3656 variant : "error" | "success" ;
3757 message : string ;
3858 t : string ;
3959 toastWidth ?: string | number ;
60+ title ?: string ;
61+ action ?: ToastMessageAction ;
4062} ) {
4163 return (
4264 < div
@@ -51,11 +73,17 @@ export function ToastUI({
5173 >
5274 < div className = "flex w-full items-start gap-2 rounded-lg p-3" >
5375 { variant === "success" ? (
54- < CheckCircleIcon className = "mt-1 size-6 min-w-6 text-success" />
76+ < CheckCircleIcon className = "mt-1 size-4 min-w-4 text-success" />
5577 ) : (
56- < ExclamationCircleIcon className = "mt-1 size-6 min-w-6 text-error" />
78+ < ExclamationCircleIcon className = "mt-1 size-4 min-w-4 text-error" />
5779 ) }
58- < Paragraph className = "py-1 text-text-bright" > { message } </ Paragraph >
80+ < div className = "flex flex-col" >
81+ { title && < Header2 className = "pt-1" > { title } </ Header2 > }
82+ < Paragraph variant = "small/dimmed" className = "py-1" >
83+ { message }
84+ </ Paragraph >
85+ < Action action = { action } toastId = { t } />
86+ </ div >
5987 < button
6088 className = "hover:bg-midnight-800 ms-auto rounded p-2 text-text-dimmed transition hover:text-text-bright"
6189 onClick = { ( ) => toast . dismiss ( t ) }
@@ -66,3 +94,38 @@ export function ToastUI({
6694 </ div >
6795 ) ;
6896}
97+
98+ function Action ( { action, toastId } : { action ?: ToastMessageAction ; toastId : string } ) {
99+ if ( ! action ) return null ;
100+
101+ switch ( action . action . type ) {
102+ case "link" : {
103+ return (
104+ < LinkButton variant = { action . variant ?? "secondary/small" } to = { action . action . path } >
105+ { action . label }
106+ </ LinkButton >
107+ ) ;
108+ }
109+ case "help" : {
110+ return (
111+ < Feedback
112+ button = {
113+ < Button
114+ variant = { action . variant ?? "secondary/small" }
115+ LeadingIcon = { EnvelopeIcon }
116+ onClick = { ( e ) => {
117+ e . preventDefault ( ) ;
118+ toast . dismiss ( toastId ) ;
119+ } }
120+ >
121+ { action . label }
122+ </ Button >
123+ }
124+ />
125+ ) ;
126+ }
127+ default : {
128+ return null ;
129+ }
130+ }
131+ }
0 commit comments