@@ -16,15 +16,18 @@ import { Button } from '@/components/ui/button'
1616import { CopyButton } from '@/components/ui/copy-button'
1717import { createLogger } from '@/lib/logs/console-logger'
1818import { cn } from '@/lib/utils'
19- import { useNotificationStore } from '@/stores/notifications/store'
19+ import {
20+ MAX_VISIBLE_NOTIFICATIONS ,
21+ NOTIFICATION_TIMEOUT ,
22+ useNotificationStore ,
23+ } from '@/stores/notifications/store'
2024import { Notification } from '@/stores/notifications/types'
2125import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
2226import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2327
2428const logger = createLogger ( 'Notifications' )
2529
2630// Constants
27- const NOTIFICATION_TIMEOUT = 4000 // Show notification for 4 seconds
2831const FADE_DURATION = 500 // Fade out over 500ms
2932
3033// Define keyframes for the animations in a style tag
@@ -52,13 +55,33 @@ const AnimationStyles = () => (
5255 }
5356 }
5457
58+ @keyframes notification-slide-up {
59+ 0% {
60+ transform: translateY(0);
61+ }
62+ 100% {
63+ transform: translateY(-100%);
64+ }
65+ }
66+
5567 .animate-notification-slide {
5668 animation: notification-slide 300ms ease forwards;
5769 }
5870
5971 .animate-notification-fade-out {
6072 animation: notification-fade-out ${ FADE_DURATION } ms ease forwards;
6173 }
74+
75+ .animate-notification-slide-up {
76+ animation: notification-slide-up 300ms ease forwards;
77+ }
78+
79+ .notification-container {
80+ transition:
81+ height 300ms ease,
82+ opacity 300ms ease,
83+ transform 300ms ease;
84+ }
6285 ` } </ style >
6386)
6487
@@ -129,12 +152,19 @@ function DeleteApiConfirmation({
129152 */
130153export function NotificationList ( ) {
131154 // Store access
132- const { notifications, hideNotification, markAsRead, removeNotification } = useNotificationStore ( )
155+ const {
156+ notifications,
157+ hideNotification,
158+ markAsRead,
159+ removeNotification,
160+ setNotificationFading,
161+ getVisibleNotificationCount,
162+ } = useNotificationStore ( )
133163 const { activeWorkflowId } = useWorkflowRegistry ( )
134164
135165 // Local state
136- const [ fadingNotifications , setFadingNotifications ] = useState < Set < string > > ( new Set ( ) )
137166 const [ removedIds , setRemovedIds ] = useState < Set < string > > ( new Set ( ) )
167+ const [ animatingIds , setAnimatingIds ] = useState < Set < string > > ( new Set ( ) )
138168
139169 // Filter to only show:
140170 // 1. Visible notifications for the current workflow
@@ -148,6 +178,10 @@ export function NotificationList() {
148178 ! removedIds . has ( n . id )
149179 )
150180
181+ // Check if we're over the limit of visible notifications
182+ const visibleCount = activeWorkflowId ? getVisibleNotificationCount ( activeWorkflowId ) : 0
183+ const isOverLimit = visibleCount > MAX_VISIBLE_NOTIFICATIONS
184+
151185 // Reset removedIds whenever a notification's visibility changes from false to true
152186 useEffect ( ( ) => {
153187 const newlyVisibleNotifications = notifications . filter (
@@ -160,51 +194,46 @@ export function NotificationList() {
160194 newlyVisibleNotifications . forEach ( ( n ) => next . delete ( n . id ) )
161195 return next
162196 } )
163-
164- // Also reset fading state for these notifications
165- setFadingNotifications ( ( prev ) => {
166- const next = new Set ( prev )
167- newlyVisibleNotifications . forEach ( ( n ) => next . delete ( n . id ) )
168- return next
169- } )
170197 }
171198 } , [ notifications , removedIds ] )
172199
173- // Handle auto-dismissal of non-persistent notifications
200+ // Handle fading notifications created by the store
174201 useEffect ( ( ) => {
175- // Setup timers for each notification
176- const timers : ReturnType < typeof setTimeout > [ ] = [ ]
202+ // This effect watches for notifications that are fading
203+ // and handles the DOM removal after animation completes
204+
205+ const timers : Record < string , ReturnType < typeof setTimeout > > = { }
177206
178207 visibleNotifications . forEach ( ( notification ) => {
179- // Skip if already hidden or marked as persistent
180- if ( ! notification . isVisible || notification . options ?. isPersistent ) return
181-
182- // Start fade out animation
183- const fadeTimer = setTimeout ( ( ) => {
184- setFadingNotifications ( ( prev ) => new Set ( [ ...prev , notification . id ] ) )
185- } , NOTIFICATION_TIMEOUT )
186-
187- // Hide notification after fade completes and mark for removal from DOM
188- const hideTimer = setTimeout ( ( ) => {
189- hideNotification ( notification . id )
190- markAsRead ( notification . id )
191-
192- // Mark this notification ID as removed to exclude it from rendering
193- setRemovedIds ( ( prev ) => new Set ( [ ...prev , notification . id ] ) )
194-
195- setFadingNotifications ( ( prev ) => {
196- const next = new Set ( prev )
197- next . delete ( notification . id )
198- return next
199- } )
200- } , NOTIFICATION_TIMEOUT + FADE_DURATION )
201-
202- timers . push ( fadeTimer , hideTimer )
208+ // For notifications that have started fading, set up cleanup timers
209+ if ( notification . isFading && ! animatingIds . has ( notification . id ) ) {
210+ // Start slide up animation after fade animation
211+ const slideTimer = setTimeout ( ( ) => {
212+ setAnimatingIds ( ( prev ) => new Set ( [ ...prev , notification . id ] ) )
213+
214+ // After slide animation, remove from DOM
215+ setTimeout ( ( ) => {
216+ hideNotification ( notification . id )
217+ markAsRead ( notification . id )
218+ setRemovedIds ( ( prev ) => new Set ( [ ...prev , notification . id ] ) )
219+
220+ // Remove from animating set
221+ setAnimatingIds ( ( prev ) => {
222+ const next = new Set ( prev )
223+ next . delete ( notification . id )
224+ return next
225+ } )
226+ } , 300 )
227+ } , FADE_DURATION )
228+
229+ timers [ notification . id ] = slideTimer
230+ }
203231 } )
204232
205- // Cleanup timers on unmount or when notifications change
206- return ( ) => timers . forEach ( clearTimeout )
207- } , [ visibleNotifications , hideNotification , markAsRead ] )
233+ return ( ) => {
234+ Object . values ( timers ) . forEach ( clearTimeout )
235+ }
236+ } , [ visibleNotifications , animatingIds , hideNotification , markAsRead ] )
208237
209238 // Early return if no notifications to show
210239 if ( visibleNotifications . length === 0 ) return null
@@ -220,26 +249,48 @@ export function NotificationList() {
220249 } }
221250 >
222251 { visibleNotifications . map ( ( notification ) => (
223- < NotificationAlert
252+ < div
224253 key = { notification . id }
225- notification = { notification }
226- isFading = { fadingNotifications . has ( notification . id ) }
227- onHide = { ( id ) => {
228- hideNotification ( id )
229- markAsRead ( id )
230- // Start the fade out animation
231- setFadingNotifications ( ( prev ) => new Set ( [ ...prev , id ] ) )
232- // Remove from DOM after animation completes
233- setTimeout ( ( ) => {
234- setRemovedIds ( ( prev ) => new Set ( [ ...prev , id ] ) )
235- setFadingNotifications ( ( prev ) => {
236- const next = new Set ( prev )
237- next . delete ( id )
238- return next
239- } )
240- } , FADE_DURATION )
241- } }
242- />
254+ className = { cn (
255+ 'notification-container' ,
256+ animatingIds . has ( notification . id ) && 'animate-notification-slide-up'
257+ ) }
258+ >
259+ < NotificationAlert
260+ notification = { notification }
261+ isFading = { notification . isFading ?? false }
262+ onHide = { ( id ) => {
263+ // For persistent notifications like API, just hide immediately without animations
264+ if ( notification . options ?. isPersistent ) {
265+ hideNotification ( id )
266+ markAsRead ( id )
267+ setRemovedIds ( ( prev ) => new Set ( [ ...prev , id ] ) )
268+ return
269+ }
270+
271+ // For regular notifications, use the animation sequence
272+ // Start the fade out animation when manually closing
273+ setNotificationFading ( id )
274+
275+ // Start slide up animation after fade completes
276+ setTimeout ( ( ) => {
277+ setAnimatingIds ( ( prev ) => new Set ( [ ...prev , id ] ) )
278+ } , FADE_DURATION )
279+
280+ // Remove from DOM after all animations complete
281+ setTimeout ( ( ) => {
282+ hideNotification ( id )
283+ markAsRead ( id )
284+ setRemovedIds ( ( prev ) => new Set ( [ ...prev , id ] ) )
285+ setAnimatingIds ( ( prev ) => {
286+ const next = new Set ( prev )
287+ next . delete ( id )
288+ return next
289+ } )
290+ } , FADE_DURATION + 300 ) // Fade + slide durations
291+ } }
292+ />
293+ </ div >
243294 ) ) }
244295 </ div >
245296 </ >
0 commit comments