add Notification function.
This commit is contained in:
		
							
								
								
									
										48
									
								
								deno.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										48
									
								
								deno.lock
									
									
									
										generated
									
									
									
								
							| @@ -11,6 +11,7 @@ | ||||
|     "npm:@tauri-apps/plugin-os@2.2": "2.2.0", | ||||
|     "npm:@types/lodash-es@^4.17.12": "4.17.12", | ||||
|     "npm:@types/react-dom@19.0.4": "19.0.4_@types+react@19.0.10", | ||||
|     "npm:@types/react-transition-group@^4.4.12": "4.4.12_@types+react@19.0.10", | ||||
|     "npm:@types/react@19.0.10": "19.0.10", | ||||
|     "npm:@types/uuid@10": "10.0.0", | ||||
|     "npm:@vitejs/plugin-react@^4.3.4": "4.3.4_vite@6.2.0__lightningcss@1.29.1_@babel+core@7.26.9_lightningcss@1.29.1", | ||||
| @@ -25,6 +26,7 @@ | ||||
|     "npm:lodash-es@^4.17.21": "4.17.21", | ||||
|     "npm:react-dom@19.0.0": "19.0.0_react@19.0.0", | ||||
|     "npm:react-router-dom@^7.2.0": "7.2.0_react@19.0.0_react-dom@19.0.0__react@19.0.0", | ||||
|     "npm:react-transition-group@^4.4.5": "4.4.5_react@19.0.0_react-dom@19.0.0__react@19.0.0", | ||||
|     "npm:react-use@^17.6.0": "17.6.0_react@19.0.0_react-dom@19.0.0__react@19.0.0_tslib@2.8.1", | ||||
|     "npm:react@19.0.0": "19.0.0", | ||||
|     "npm:sanitize.css@13": "13.0.0", | ||||
| @@ -561,6 +563,12 @@ | ||||
|         "@types/react" | ||||
|       ] | ||||
|     }, | ||||
|     "@types/react-transition-group@4.4.12_@types+react@19.0.10": { | ||||
|       "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", | ||||
|       "dependencies": [ | ||||
|         "@types/react" | ||||
|       ] | ||||
|     }, | ||||
|     "@types/react@19.0.10": { | ||||
|       "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", | ||||
|       "dependencies": [ | ||||
| @@ -803,6 +811,13 @@ | ||||
|     "detect-libc@1.0.3": { | ||||
|       "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" | ||||
|     }, | ||||
|     "dom-helpers@5.2.1": { | ||||
|       "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", | ||||
|       "dependencies": [ | ||||
|         "@babel/runtime", | ||||
|         "csstype" | ||||
|       ] | ||||
|     }, | ||||
|     "electron-to-chromium@1.5.104": { | ||||
|       "integrity": "sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==" | ||||
|     }, | ||||
| @@ -1173,6 +1188,12 @@ | ||||
|     "lodash.merge@4.6.2": { | ||||
|       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" | ||||
|     }, | ||||
|     "loose-envify@1.4.0": { | ||||
|       "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", | ||||
|       "dependencies": [ | ||||
|         "js-tokens" | ||||
|       ] | ||||
|     }, | ||||
|     "lru-cache@5.1.1": { | ||||
|       "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", | ||||
|       "dependencies": [ | ||||
| @@ -1231,6 +1252,9 @@ | ||||
|     "node-releases@2.0.19": { | ||||
|       "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" | ||||
|     }, | ||||
|     "object-assign@4.1.1": { | ||||
|       "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" | ||||
|     }, | ||||
|     "optionator@0.9.4": { | ||||
|       "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", | ||||
|       "dependencies": [ | ||||
| @@ -1283,6 +1307,14 @@ | ||||
|     "prelude-ls@1.2.1": { | ||||
|       "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" | ||||
|     }, | ||||
|     "prop-types@15.8.1": { | ||||
|       "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", | ||||
|       "dependencies": [ | ||||
|         "loose-envify", | ||||
|         "object-assign", | ||||
|         "react-is" | ||||
|       ] | ||||
|     }, | ||||
|     "punycode@2.3.1": { | ||||
|       "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" | ||||
|     }, | ||||
| @@ -1296,6 +1328,9 @@ | ||||
|         "scheduler" | ||||
|       ] | ||||
|     }, | ||||
|     "react-is@16.13.1": { | ||||
|       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" | ||||
|     }, | ||||
|     "react-refresh@0.14.2": { | ||||
|       "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==" | ||||
|     }, | ||||
| @@ -1318,6 +1353,17 @@ | ||||
|         "turbo-stream" | ||||
|       ] | ||||
|     }, | ||||
|     "react-transition-group@4.4.5_react@19.0.0_react-dom@19.0.0__react@19.0.0": { | ||||
|       "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", | ||||
|       "dependencies": [ | ||||
|         "@babel/runtime", | ||||
|         "dom-helpers", | ||||
|         "loose-envify", | ||||
|         "prop-types", | ||||
|         "react", | ||||
|         "react-dom" | ||||
|       ] | ||||
|     }, | ||||
|     "react-universal-interface@0.6.2_react@19.0.0_tslib@2.8.1": { | ||||
|       "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", | ||||
|       "dependencies": [ | ||||
| @@ -1576,6 +1622,7 @@ | ||||
|         "npm:@tauri-apps/plugin-os@2.2", | ||||
|         "npm:@types/lodash-es@^4.17.12", | ||||
|         "npm:@types/react-dom@19.0.4", | ||||
|         "npm:@types/react-transition-group@^4.4.12", | ||||
|         "npm:@types/react@19.0.10", | ||||
|         "npm:@types/uuid@10", | ||||
|         "npm:@vitejs/plugin-react@^4.3.4", | ||||
| @@ -1590,6 +1637,7 @@ | ||||
|         "npm:lodash-es@^4.17.21", | ||||
|         "npm:react-dom@19.0.0", | ||||
|         "npm:react-router-dom@^7.2.0", | ||||
|         "npm:react-transition-group@^4.4.5", | ||||
|         "npm:react-use@^17.6.0", | ||||
|         "npm:react@19.0.0", | ||||
|         "npm:sanitize.css@13", | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
|     "react": "19.0.0", | ||||
|     "react-dom": "19.0.0", | ||||
|     "react-router-dom": "^7.2.0", | ||||
|     "react-transition-group": "^4.4.5", | ||||
|     "react-use": "^17.6.0", | ||||
|     "sanitize.css": "^13.0.0", | ||||
|     "@tauri-apps/api": "^2", | ||||
| @@ -31,6 +32,7 @@ | ||||
|   "devDependencies": { | ||||
|     "@eslint/js": "^9.19.0", | ||||
|     "@types/lodash-es": "^4.17.12", | ||||
|     "@types/react-transition-group": "^4.4.12", | ||||
|     "@types/uuid": "^10.0.0", | ||||
|     "eslint": "^9.19.0", | ||||
|     "eslint-plugin-react-hooks": "^5.2.0", | ||||
|   | ||||
							
								
								
									
										132
									
								
								src/components/Notifications.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/components/Notifications.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| @layer components { | ||||
|   .notification_positioner { | ||||
|     position: absolute; | ||||
|     bottom: 20px; | ||||
|     right: 20px; | ||||
|     width: 25vw; | ||||
|     display: flex; | ||||
|     flex-direction: column-reverse; | ||||
|     justify-content: flex-start; | ||||
|     align-items: stretch; | ||||
|     gap: 10px; | ||||
|     z-index: 600; | ||||
|   } | ||||
|   .message_box_positioner { | ||||
|     position: absolute; | ||||
|   } | ||||
|   .toast_positioner { | ||||
|     position: absolute; | ||||
|     left: 50%; | ||||
|     transform: translateX(-50%); | ||||
|     bottom: 20vh; | ||||
|     z-index: 700; | ||||
|   } | ||||
|   .notification { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: flex-start; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     padding: 10px 15px; | ||||
|     border-radius: 2px; | ||||
|     .notification_icon { | ||||
|       flex-shrink: 0; | ||||
|       font-size: 24px; | ||||
|     } | ||||
|     .notificaiton_content { | ||||
|       flex-grow: 1; | ||||
|       .notification_title { | ||||
|         font-size: 1.4em; | ||||
|         font-weight: bold; | ||||
|       } | ||||
|     } | ||||
|     .notification_close { | ||||
|       flex-shrink: 0; | ||||
|       font-size: 16px; | ||||
|       align-self: flex-start; | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
|   .toast { | ||||
|     position: absolute; | ||||
|     bottom: 0; | ||||
|     left: 50%; | ||||
|     transform: translateX(-50%); | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: flex-start; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     padding: 4px 8px; | ||||
|     border-radius: 2px; | ||||
|     width: 35vw; | ||||
|     .toast_icon { | ||||
|       flex-shrink: 0; | ||||
|       font-size: 18px; | ||||
|     } | ||||
|     .toast_content { | ||||
|       flex-grow: 1; | ||||
|       display: inline-block; | ||||
|       overflow-wrap: break-word; | ||||
|     } | ||||
|     .toast_close { | ||||
|       flex-shrink: 0; | ||||
|       font-size: 14px; | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
|   .kind_info { | ||||
|     color: var(--color-on-info-container); | ||||
|     background-color: var(--color-info-container); | ||||
|     box-shadow: var(--elevation-2-ambient) var(--elevation-2-umbra); | ||||
|   } | ||||
|   .kind_promption { | ||||
|     color: var(--color-on-tertiary-container); | ||||
|     background-color: var(--color-tertiary-container); | ||||
|     box-shadow: var(--elevation-2-ambient) var(--elevation-2-umbra); | ||||
|   } | ||||
|   .kind_success { | ||||
|     color: var(--color-on-success-container); | ||||
|     background-color: var(--color-success-container); | ||||
|     box-shadow: var(--elevation-2-ambient) var(--elevation-2-umbra); | ||||
|   } | ||||
|   .kind_warning { | ||||
|     color: var(--color-on-warning-container); | ||||
|     background-color: var(--color-warning-container); | ||||
|     box-shadow: var(--elevation-2-ambient) var(--elevation-2-umbra); | ||||
|   } | ||||
|   .kind_error { | ||||
|     color: var(--color-on-error-container); | ||||
|     background-color: var(--color-error-container); | ||||
|     box-shadow: var(--elevation-2-ambient) var(--elevation-2-umbra); | ||||
|   } | ||||
|   .kind_custom { | ||||
|     color: var(--color-on-defensive-container); | ||||
|     background-color: var(--color-defensive-container); | ||||
|     box-shadow: var(--elevation-2-ambient) var(--elevation-2-umbra); | ||||
|   } | ||||
|   .slide_in_enter { | ||||
|     transform: translateX(100%); | ||||
|     opacity: 0; | ||||
|   } | ||||
|   .slide_in_enter_active { | ||||
|     transition: all 500ms ease-in-out; | ||||
|     transform: translateX(0); | ||||
|     opacity: 1; | ||||
|   } | ||||
|   .slide_out_exit_active { | ||||
|     transition: all 500ms ease-in-out; | ||||
|     opacity: 0; | ||||
|   } | ||||
|   .fade_in_enter { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   .fade_in_enter_active { | ||||
|     transition: all 500ms ease-in-out; | ||||
|     opacity: 1; | ||||
|   } | ||||
|   .fade_out_exit_active { | ||||
|     transition: all 500ms ease-in-out; | ||||
|     opacity: 0; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										296
									
								
								src/components/Notifications.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								src/components/Notifications.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,296 @@ | ||||
| import { Icon, IconifyIconProps } from '@iconify/react/dist/iconify.js'; | ||||
| import cx from 'clsx'; | ||||
| import { | ||||
|   createContext, | ||||
|   createRef, | ||||
|   FC, | ||||
|   ReactNode, | ||||
|   RefObject, | ||||
|   useCallback, | ||||
|   useContext, | ||||
|   useEffect, | ||||
|   useState, | ||||
| } from 'react'; | ||||
| import { createPortal } from 'react-dom'; | ||||
| import { CSSTransition, TransitionGroup } from 'react-transition-group'; | ||||
| import { v4 } from 'uuid'; | ||||
| import styles from './Notifications.module.css'; | ||||
|  | ||||
| export enum NotificationType { | ||||
|   INFO, | ||||
|   PROMPTION, | ||||
|   SUCCESS, | ||||
|   WARNING, | ||||
|   ERROR, | ||||
|   CUSTOM, | ||||
| } | ||||
|  | ||||
| export enum ToastDuration { | ||||
|   MANUAL = 0, | ||||
|   SHORT = 1500, | ||||
|   MEDIUM = 3000, | ||||
|   LONG = 5000, | ||||
| } | ||||
|  | ||||
| interface NotificationFunctions { | ||||
|   addNotification( | ||||
|     kind: NotificationType, | ||||
|     title?: string, | ||||
|     message?: ReactNode, | ||||
|     icon?: IconifyIconProps['icon'], | ||||
|     duration?: number, | ||||
|   ): string; | ||||
|   removeNotification(id: string): void; | ||||
|   showToast( | ||||
|     kind: NotificationType, | ||||
|     message: string, | ||||
|     icon?: IconifyIconProps['icon'], | ||||
|     duration?: ToastDuration, | ||||
|   ): string; | ||||
| } | ||||
|  | ||||
| const NotificationStyleMap = { | ||||
|   [NotificationType.INFO]: styles.kind_info, | ||||
|   [NotificationType.PROMPTION]: styles.kind_promption, | ||||
|   [NotificationType.SUCCESS]: styles.kind_success, | ||||
|   [NotificationType.WARNING]: styles.kind_warning, | ||||
|   [NotificationType.ERROR]: styles.kind_error, | ||||
|   [NotificationType.CUSTOM]: styles.kind_custom, | ||||
| }; | ||||
|  | ||||
| const NotificationHostContext = createContext<NotificationFunctions>({ | ||||
|   addNotification: () => '', | ||||
|   removeNotification: () => {}, | ||||
|   showToast: () => '', | ||||
| }); | ||||
|  | ||||
| type NotificationProps = { | ||||
|   kind?: NotificationType; | ||||
|   nid: string; | ||||
|   icon?: IconifyIconProps['icon']; | ||||
|   title?: string; | ||||
|   message?: ReactNode; | ||||
|   duration?: number; | ||||
|   ref: RefObject<HTMLDivElement>; | ||||
|   closeAction?: (id: string) => void; | ||||
| }; | ||||
|  | ||||
| const Notification: FC<NotificationProps> = ({ | ||||
|   kind = NotificationType.INFO, | ||||
|   nid, | ||||
|   icon, | ||||
|   title, | ||||
|   message, | ||||
|   duration = 3000, | ||||
|   ref, | ||||
|   closeAction, | ||||
| }) => { | ||||
|   useEffect(() => { | ||||
|     if (duration > 0) { | ||||
|       const timer = setTimeout(() => { | ||||
|         closeAction?.(nid); | ||||
|       }, duration); | ||||
|       return () => clearTimeout(timer); | ||||
|     } | ||||
|   }, [nid, duration, closeAction]); | ||||
|  | ||||
|   return ( | ||||
|     <div className={cx(styles.notification, NotificationStyleMap[kind])} ref={ref}> | ||||
|       {icon && <Icon icon={icon} className={cx(styles.notification_icon)} />} | ||||
|       <div className={cx(styles.notificaiton_content)}> | ||||
|         {title && <div className={cx(styles.notification_title)}>{title}</div>} | ||||
|         {message && <div>{message}</div>} | ||||
|       </div> | ||||
|       {duration === 0 && ( | ||||
|         <Icon | ||||
|           icon="material-symbols-light:close" | ||||
|           className={styles.notification_close} | ||||
|           onClick={() => closeAction?.(nid)} | ||||
|         /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| type ToastProps = { | ||||
|   kind: NotificationType; | ||||
|   tid: string; | ||||
|   message?: string; | ||||
|   icon?: string; | ||||
|   duration?: ToastDuration; | ||||
|   ref: RefObject<HTMLDivElement>; | ||||
|   closeAction: (tid?: string) => void; | ||||
| }; | ||||
|  | ||||
| const Toast: FC<ToastProps> = ({ | ||||
|   kind, | ||||
|   tid, | ||||
|   message, | ||||
|   icon, | ||||
|   duration = ToastDuration.MEDIUM, | ||||
|   ref, | ||||
|   closeAction, | ||||
| }) => { | ||||
|   useEffect(() => { | ||||
|     if (duration !== ToastDuration.MANUAL) { | ||||
|       const timer = setTimeout(() => { | ||||
|         closeAction?.(tid); | ||||
|       }, duration); | ||||
|       return () => clearTimeout(timer); | ||||
|     } | ||||
|   }, [tid, duration, closeAction]); | ||||
|  | ||||
|   return ( | ||||
|     <div className={cx(styles.toast, NotificationStyleMap[kind])} ref={ref}> | ||||
|       {icon && <Icon icon={icon} className={styles.toast_icon} />} | ||||
|       <div className={styles.toast_content}>{message}</div> | ||||
|       {duration === ToastDuration.MANUAL && ( | ||||
|         <Icon | ||||
|           icon="material-symbols-light:close" | ||||
|           className={styles.toast_close} | ||||
|           onClick={() => closeAction?.(tid)} | ||||
|         /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export function useNotification() { | ||||
|   const functions = useContext<NotificationFunctions>(NotificationHostContext); | ||||
|   return functions; | ||||
| } | ||||
|  | ||||
| type NotificationElement = { | ||||
|   id: string; | ||||
|   element: ReactNode; | ||||
|   ref: RefObject<ReactNode | HTMLDivElement>; | ||||
| }; | ||||
|  | ||||
| type NotificationsProps = { | ||||
|   defaultDuration?: number; | ||||
|   maxNotifications?: number; | ||||
|   children?: ReactNode; | ||||
| }; | ||||
|  | ||||
| const Notifications: FC<NotificationsProps> = ({ | ||||
|   defaultDuration = 3000, | ||||
|   maxNotifications = 5, | ||||
|   children, | ||||
| }) => { | ||||
|   const [notifications, setNotifications] = useState<NotificationElement[]>([]); | ||||
|   const [toasts, setToasts] = useState<NotificationElement[]>([]); | ||||
|  | ||||
|   const removeNotification = useCallback((id: string) => { | ||||
|     setNotifications((prev) => prev.filter((n) => n.id !== id)); | ||||
|   }, []); | ||||
|   const addNotification = useCallback( | ||||
|     ( | ||||
|       kind: NotificationType, | ||||
|       title?: string, | ||||
|       message?: ReactNode, | ||||
|       icon?: IconifyIconProps['icon'], | ||||
|       duration?: number, | ||||
|     ) => { | ||||
|       const id = v4(); | ||||
|       const ref = createRef<ReactNode | HTMLDivElement>(); | ||||
|       const newNotify = ( | ||||
|         <Notification | ||||
|           kind={kind} | ||||
|           nid={id} | ||||
|           icon={icon} | ||||
|           title={title} | ||||
|           message={message} | ||||
|           duration={duration ?? defaultDuration} | ||||
|           closeAction={removeNotification} | ||||
|           //@ts-expect-error TS2322 | ||||
|           ref={ref} | ||||
|         /> | ||||
|       ); | ||||
|       setNotifications((prev) => [...prev, { id, element: newNotify, ref } as NotificationElement]); | ||||
|  | ||||
|       return id; | ||||
|     }, | ||||
|     [removeNotification, defaultDuration], | ||||
|   ); | ||||
|   const removeToast = useCallback((id: string) => { | ||||
|     setToasts((prev) => prev.filter((n) => n.id !== id)); | ||||
|   }, []); | ||||
|   const showToast = useCallback( | ||||
|     (kind: NotificationType, message?: string, icon?: string, duration?: ToastDuration) => { | ||||
|       const id = v4(); | ||||
|       const ref = createRef<HTMLDivElement>(); | ||||
|       const newToast = ( | ||||
|         <Toast | ||||
|           kind={kind} | ||||
|           tid={id} | ||||
|           message={message ?? ''} | ||||
|           icon={icon} | ||||
|           duration={duration ?? ToastDuration.MEDIUM} | ||||
|           ref={ref} | ||||
|           closeAction={removeToast} | ||||
|         /> | ||||
|       ); | ||||
|       setToasts((prev) => [...prev, { id, element: newToast, ref } as NotificationElement]); | ||||
|  | ||||
|       return id; | ||||
|     }, | ||||
|     [removeToast], | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <NotificationHostContext | ||||
|       value={{ | ||||
|         addNotification, | ||||
|         removeNotification, | ||||
|         showToast, | ||||
|       }}> | ||||
|       {children} | ||||
|       {createPortal( | ||||
|         <div className={cx(styles.notification_positioner)}> | ||||
|           <TransitionGroup component={null}> | ||||
|             {notifications.slice(0, maxNotifications).map(({ id, element, ref }) => ( | ||||
|               <CSSTransition | ||||
|                 key={id} | ||||
|                 //@ts-expect-error TS2322 | ||||
|                 nodeRef={ref} | ||||
|                 unmountOnExit | ||||
|                 timeout={500} | ||||
|                 classNames={{ | ||||
|                   enter: styles.slide_in_enter, | ||||
|                   enterActive: styles.slide_in_enter_active, | ||||
|                   exitActive: styles.slide_out_exit_active, | ||||
|                 }}> | ||||
|                 {element} | ||||
|               </CSSTransition> | ||||
|             ))} | ||||
|           </TransitionGroup> | ||||
|         </div>, | ||||
|         document.body, | ||||
|       )} | ||||
|       {createPortal( | ||||
|         <div className={cx(styles.toast_positioner)}> | ||||
|           <TransitionGroup component={null}> | ||||
|             {toasts.slice(0, 1).map(({ id, element, ref }) => ( | ||||
|               <CSSTransition | ||||
|                 key={id} | ||||
|                 //@ts-expect-error TS2322 | ||||
|                 nodeRef={ref} | ||||
|                 unmountOnExit | ||||
|                 timeout={500} | ||||
|                 classNames={{ | ||||
|                   enter: styles.fade_in_enter, | ||||
|                   enterActive: styles.fade_in_enter_active, | ||||
|                   exitActive: styles.fade_out_exit_active, | ||||
|                 }}> | ||||
|                 {element} | ||||
|               </CSSTransition> | ||||
|             ))} | ||||
|           </TransitionGroup> | ||||
|         </div>, | ||||
|         document.body, | ||||
|       )} | ||||
|     </NotificationHostContext> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Notifications; | ||||
							
								
								
									
										35
									
								
								src/main.tsx
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								src/main.tsx
									
									
									
									
									
								
							| @@ -5,27 +5,36 @@ import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import { BrowserRouter, Route, Routes } from 'react-router-dom'; | ||||
| import Layout from './Layout'; | ||||
| import Notifications from './components/Notifications'; | ||||
| import EstimWatchProvider from './context/EstimContext'; | ||||
| import CreatePattern from './pages/CreatePattern'; | ||||
| import Device from './pages/Device'; | ||||
| import PatternEditor from './pages/PatternEditor'; | ||||
| import PatternLibrary from './pages/PatternLibrary'; | ||||
| import PatternNavigator from './pages/PatternNavigator'; | ||||
| import PlayControl from './pages/Play'; | ||||
| import Settings from './pages/Settings'; | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | ||||
|   <React.StrictMode> | ||||
|     <EstimWatchProvider> | ||||
|       <BrowserRouter> | ||||
|         <Routes> | ||||
|           <Route path="/" element={<Layout />}> | ||||
|             <Route index element={<Device />} /> | ||||
|             <Route path="/play" element={<PlayControl />} /> | ||||
|             <Route path="/library" element={<PatternLibrary />} /> | ||||
|             <Route path="/pattern-editor" element={<PatternEditor />} /> | ||||
|             <Route path="/settings" element={<Settings />} /> | ||||
|           </Route> | ||||
|         </Routes> | ||||
|       </BrowserRouter> | ||||
|     </EstimWatchProvider> | ||||
|     <Notifications> | ||||
|       <EstimWatchProvider> | ||||
|         <BrowserRouter> | ||||
|           <Routes> | ||||
|             <Route path="/" element={<Layout />}> | ||||
|               <Route index element={<Device />} /> | ||||
|               <Route path="/play" element={<PlayControl />} /> | ||||
|               <Route path="/library" element={<PatternLibrary />} /> | ||||
|               <Route path="/pattern-editor"> | ||||
|                 <Route index element={<PatternNavigator />} /> | ||||
|                 <Route path="new" element={<CreatePattern />} /> | ||||
|                 <Route path=":pattern" element={<PatternEditor />} /> | ||||
|               </Route> | ||||
|               <Route path="/settings" element={<Settings />} /> | ||||
|             </Route> | ||||
|           </Routes> | ||||
|         </BrowserRouter> | ||||
|       </EstimWatchProvider> | ||||
|     </Notifications> | ||||
|   </React.StrictMode>, | ||||
| ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user