@@ -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,48 @@ 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 . variant === 'success' && (
153+ < span className = 'mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-success)] align-middle' />
154+ ) }
155+ { t . message }
156+ </ div >
157+ < div className = 'flex shrink-0 items-start gap-[2px]' >
158+ { t . duration > 0 && < CountdownRing duration = { t . duration } /> }
159+ < button
160+ type = 'button'
161+ onClick = { dismiss }
162+ aria-label = 'Dismiss notification'
163+ className = '-m-[2px] shrink-0 rounded-[5px] p-[4px] hover:bg-[var(--surface-active)]'
164+ >
165+ < X className = 'h-[14px] w-[14px] text-[var(--text-icon)]' />
166+ </ button >
167+ </ div >
168+ </ div >
169+ { t . action && (
170+ < button
171+ type = 'button'
172+ onClick = { ( ) => {
173+ t . action ! . onClick ( )
174+ dismiss ( )
175+ } }
176+ className = 'w-full rounded-[5px] bg-[var(--surface-active)] px-[8px] py-[4px] font-medium text-[12px] hover:bg-[var(--surface-hover)]'
177+ >
178+ { t . action . label }
179+ </ button >
131180 ) }
132181 </ 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 >
152182 </ div >
153183 )
154184}
@@ -175,7 +205,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
175205 const data : ToastData = {
176206 id,
177207 message : input . message ,
178- description : input . description ,
179208 variant : input . variant ?? 'default' ,
180209 action : input . action ,
181210 duration : input . duration ?? AUTO_DISMISS_MS ,
0 commit comments