@@ -18,6 +18,9 @@ const AUTO_DISMISS_MS = 0
1818const EXIT_ANIMATION_MS = 200
1919const MAX_VISIBLE = 20
2020
21+ const RING_RADIUS = 5.5
22+ const RING_CIRCUMFERENCE = 2 * Math . PI * RING_RADIUS
23+
2124type ToastVariant = 'default' | 'success' | 'error'
2225
2326interface ToastAction {
@@ -28,15 +31,13 @@ interface ToastAction {
2831interface ToastData {
2932 id : string
3033 message : string
31- description ?: string
3234 variant : ToastVariant
3335 action ?: ToastAction
3436 duration : number
3537}
3638
3739type ToastInput = {
3840 message : string
39- description ?: string
4041 variant ?: ToastVariant
4142 action ?: ToastAction
4243 duration ?: number
@@ -90,12 +91,31 @@ export function useToast() {
9091 return ctx
9192}
9293
93- const VARIANT_STYLES : Record < ToastVariant , string > = {
94- default : 'border-[var(--border)] bg-[var(--bg)] text-[var(--text-primary)]' ,
95- success :
96- 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-800/40 dark:bg-emerald-950/30 dark:text-emerald-200' ,
97- error :
98- 'border-red-200 bg-red-50 text-red-900 dark:border-red-800/40 dark:bg-red-950/30 dark:text-red-200' ,
94+ function CountdownRing ( { duration } : { duration : number } ) {
95+ return (
96+ < svg
97+ width = '14'
98+ height = '14'
99+ viewBox = '0 0 16 16'
100+ fill = 'none'
101+ xmlns = 'http://www.w3.org/2000/svg'
102+ style = { { transform : 'rotate(-90deg) scaleX(-1)' } }
103+ >
104+ < circle cx = '8' cy = '8' r = { RING_RADIUS } stroke = 'var(--border)' strokeWidth = '1.5' />
105+ < circle
106+ cx = '8'
107+ cy = '8'
108+ r = { RING_RADIUS }
109+ stroke = 'var(--text-icon)'
110+ strokeWidth = '1.5'
111+ strokeLinecap = 'round'
112+ strokeDasharray = { RING_CIRCUMFERENCE }
113+ style = { {
114+ animation : `notification-countdown ${ duration } ms linear forwards` ,
115+ } }
116+ />
117+ </ svg >
118+ )
99119}
100120
101121function ToastItem ( { toast : t , onDismiss } : { toast : ToastData ; onDismiss : ( id : string ) => void } ) {
@@ -117,38 +137,45 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:
117137 return (
118138 < div
119139 className = { cn (
120- 'pointer-events-auto flex w-[320px] items-start gap-[8px] rounded-[8px] border px-[12px] py-[10px] shadow-md transition-all' ,
121- VARIANT_STYLES [ t . variant ] ,
140+ 'pointer-events-auto w-[240px] overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--bg)] shadow-sm' ,
122141 exiting
123142 ? 'animate-[toast-exit_200ms_ease-in_forwards]'
124143 : 'animate-[toast-enter_200ms_ease-out_forwards]'
125144 ) }
126145 >
127- < div className = 'min-w-0 flex-1' >
128- < p className = 'font-medium text-[13px] leading-[18px]' > { t . message } </ p >
129- { t . description && (
130- < p className = 'mt-[2px] text-[12px] leading-[16px] opacity-80' > { t . description } </ p >
146+ < div className = 'flex flex-col gap-[8px] p-[8px]' >
147+ < div className = 'flex items-start gap-[8px]' >
148+ < div className = 'line-clamp-2 min-w-0 flex-1 font-medium text-[12px] text-[var(--text-body)]' >
149+ { t . variant === 'error' && (
150+ < span className = 'mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-error)] align-middle' />
151+ ) }
152+ { t . message }
153+ </ div >
154+ < div className = 'flex shrink-0 items-start gap-[2px]' >
155+ { t . duration > 0 && < CountdownRing duration = { t . duration } /> }
156+ < button
157+ type = 'button'
158+ onClick = { dismiss }
159+ aria-label = 'Dismiss notification'
160+ className = '-m-[2px] shrink-0 rounded-[5px] p-[4px] hover:bg-[var(--surface-active)]'
161+ >
162+ < X className = 'h-[14px] w-[14px] text-[var(--text-icon)]' />
163+ </ button >
164+ </ div >
165+ </ div >
166+ { t . action && (
167+ < button
168+ type = 'button'
169+ onClick = { ( ) => {
170+ t . action ! . onClick ( )
171+ dismiss ( )
172+ } }
173+ className = 'w-full rounded-[5px] bg-[var(--surface-active)] px-[8px] py-[4px] font-medium text-[12px] hover:bg-[var(--surface-hover)]'
174+ >
175+ { t . action . label }
176+ </ button >
131177 ) }
132178 </ div >
133- { t . action && (
134- < button
135- type = 'button'
136- onClick = { ( ) => {
137- t . action ! . onClick ( )
138- dismiss ( )
139- } }
140- className = 'shrink-0 font-medium text-[13px] underline underline-offset-2 opacity-90 hover:opacity-100'
141- >
142- { t . action . label }
143- </ button >
144- ) }
145- < button
146- type = 'button'
147- onClick = { dismiss }
148- className = 'shrink-0 rounded-[4px] p-[2px] opacity-60 hover:opacity-100'
149- >
150- < X className = 'h-[14px] w-[14px]' />
151- </ button >
152179 </ div >
153180 )
154181}
@@ -175,7 +202,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
175202 const data : ToastData = {
176203 id,
177204 message : input . message ,
178- description : input . description ,
179205 variant : input . variant ?? 'default' ,
180206 action : input . action ,
181207 duration : input . duration ?? AUTO_DISMISS_MS ,
0 commit comments