diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx
index 5482323..b6704a0 100644
--- a/demo/DemoApp.tsx
+++ b/demo/DemoApp.tsx
@@ -33,6 +33,8 @@ import {DemoModal} from "./DemoModal";
import {useCallableModal} from "../src/Components/Modals/Modals";
import {ModalType} from "../src/Components/Modals/ModalsTypes";
import {Buttons} from "../src/Components/Buttons/Buttons";
+import {useNotify} from "../src/Components/Notifications/Notifications";
+import {Notification, NotificationType} from "../src/Components/Notifications/Notification";
export function DemoApp()
{
@@ -48,6 +50,8 @@ export function DemoApp()
// Easy modal.
const easyModal = useCallableModal((type: ModalType = ModalType.NONE) => );
+ const notify = useNotify();
+
const [datetime, setDatetime] = useState(null);
const [selected, setSelected] = useState(null);
@@ -506,11 +510,11 @@ export function DemoApp()
Notifications
-
-
-
-
-
+
+
+
+
+
diff --git a/src/Application/Kernel.tsx b/src/Application/Kernel.tsx
index c2c9aab..f5ff55f 100644
--- a/src/Application/Kernel.tsx
+++ b/src/Application/Kernel.tsx
@@ -2,6 +2,7 @@ import { IconContext } from "@phosphor-icons/react";
import React from "react";
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import {Curtains} from "../Components/Curtains/Curtains";
+import {NotificationsProvider} from "../Components/Notifications/Notifications";
/**
* Main Kernel UI app component which initializes everything.
@@ -17,11 +18,13 @@ export function Kernel({header, footer, router}: {
size: 16,
weight: "bold",
}}>
-
- {header}
-
- {footer}
-
+
+
+ {header}
+
+ {footer}
+
+
);
}
diff --git a/src/Components/Notifications/Notification.tsx b/src/Components/Notifications/Notification.tsx
new file mode 100644
index 0000000..0b85dd1
--- /dev/null
+++ b/src/Components/Notifications/Notification.tsx
@@ -0,0 +1,41 @@
+import React, {useCallback, useContext} from "react";
+import {classes} from "../../Utils";
+import {NotificationContext, NotificationsContext} from "./Notifications";
+
+/**
+ * Notifications types enumeration.
+ */
+export enum NotificationType
+{
+ NONE = "none",
+ INFO = "info",
+ SUCCESS = "success",
+ WARNING = "warning",
+ ERROR = "error",
+}
+
+/**
+ * Notification component.
+ */
+export function Notification({type, children}: React.PropsWithChildren<{
+ type?: NotificationType;
+}>)
+{
+ // Default type is NONE.
+ type = type ?? NotificationType.NONE;
+
+ // Get notifications context.
+ const {close} = useContext(NotificationsContext);
+
+ // Get current notification UUID.
+ const {uuid, closed} = useContext(NotificationContext);
+
+ // Initialize close notification function.
+ const closeNotification = useCallback(() => { close(uuid); }, [uuid]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/Components/Notifications/Notifications.tsx b/src/Components/Notifications/Notifications.tsx
new file mode 100644
index 0000000..b9704a2
--- /dev/null
+++ b/src/Components/Notifications/Notifications.tsx
@@ -0,0 +1,217 @@
+import React, {startTransition, useCallback, useContext, useEffect, useMemo, useRef, useState} from "react";
+import ReactDOM from "react-dom";
+import {v4 as uuidv4} from "uuid";
+
+/**
+ * Notification UUID type.
+ */
+export type NotificationUuid = string;
+
+/**
+ * Notification data.
+ */
+export interface NotificationData
+{
+ /**
+ * The notification content.
+ */
+ content: React.ReactNode;
+}
+
+/**
+ * Type of notification emitter function.
+ */
+export type EmitNotificationFunction = (content: React.ReactNode) => void;
+
+/**
+ * Type of notification close function.
+ */
+export type RemoveNotificationFunction = (uuid: NotificationUuid) => void;
+
+/**
+ * Type of notification close function.
+ */
+export type CloseNotificationFunction = (uuid: NotificationUuid) => void;
+
+/**
+ * Type of notification closed state function.
+ */
+export type IsNotificationClosedFunction = (uuid: NotificationUuid) => boolean;
+
+
+/**
+ * Interface of notifications state.
+ */
+export interface NotificationsContextState
+{
+ /**
+ * Notification function.
+ * @param content Notification content.
+ */
+ notify: EmitNotificationFunction;
+
+ /**
+ * Close notification function.
+ * @param uuid UUID of notification to close.
+ */
+ close: CloseNotificationFunction;
+
+ /**
+ * Is given notification closed?
+ * @param uuid UUID of notification to get closed state.
+ */
+ isClosed: IsNotificationClosedFunction;
+}
+
+export const NotificationsContext = React.createContext({
+ notify() {},
+ close() {},
+ isClosed() { return false; },
+});
+
+/**
+ * Hook to emit a notification.
+ */
+export function useNotify(): EmitNotificationFunction
+{
+ return useContext(NotificationsContext).notify;
+}
+
+
+
+/**
+ * Notifications provider.
+ */
+export function NotificationsProvider({children}: React.PropsWithChildren<{}>)
+{
+ // Notifications.
+ const [notifications, setNotifications] = useState>({});
+ // Keeping track of closed notifications that are still on (while transitioning out).
+ const [closedNotifications, setClosedNotifications] = useState>()
+
+ // Initialize remove notification function.
+ const remove = useRef();
+ remove.current = useCallback((uuid) => {
+ // Copy the notifications list.
+ const newNotifications = {...notifications};
+ const newClosedNotifications = {...closedNotifications};
+ // Remove the given notification from the list.
+ delete newNotifications[uuid];
+ delete newClosedNotifications[uuid];
+ // Set the new notifications list.
+ setNotifications(newNotifications);
+ setClosedNotifications(newClosedNotifications);
+ }, [notifications, setNotifications, closedNotifications, setClosedNotifications]);
+
+ // Initialize close notification function with animation.
+ const close = useRef();
+ close.current = useCallback((uuid) => {
+ // Add the given curtain UUID to the list of closed curtains.
+ setClosedNotifications({
+ ...closedNotifications,
+ [uuid]: true,
+ });
+
+ // Remove the notification 300ms later.
+ window.setTimeout(() => {
+ // Remove the curtain.
+ remove.current(uuid);
+ }, 300);
+ }, [remove, closedNotifications, setClosedNotifications]);
+
+ // Initialize isClosed notification function.
+ const isClosed = useRef();
+ isClosed.current = useCallback((uuid) => (!!closedNotifications?.[uuid]), [closedNotifications]);
+
+ // Initialize notify function.
+ const notify = useRef();
+ notify.current = useCallback((content: React.ReactNode) => {
+ // Generate a new notification UUID for the new notification.
+ const notificationUuid = uuidv4();
+
+ // Add the notification to the list of notifications, with the generated UUID.
+ setNotifications({
+ ...notifications,
+ [notificationUuid]: {
+ content: content,
+ },
+ });
+
+ // Close notification 10s later.
+ setTimeout(() => {
+ close.current(notificationUuid);
+ }, 10000);
+ }, [notifications, setNotifications]);
+
+ // Initialize context state from action functions.
+ const contextState = useMemo(() => ({
+ notify: (content: React.ReactNode) => notify.current(content),
+ close: (uuid: NotificationUuid) => close.current(uuid),
+ isClosed: (uuid: NotificationUuid) => isClosed.current(uuid),
+ }), [notify, isClosed]);
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+/**
+ * Curtains portal manager.
+ */
+function NotificationsPortal({notifications}: {
+ notifications: Record;
+})
+{
+ return ReactDOM.createPortal((
+
+ { // Show notifications list.
+ Object.entries(notifications).map(([uuid, notificationData]) => (
+
+ {notificationData.content}
+
+ ))
+ }
+
+ ), document.body);
+}
+
+/**
+ * A notification context.
+ */
+export const NotificationContext = React.createContext<{
+ /**
+ * Notification UUID.
+ */
+ uuid: NotificationUuid;
+
+ /**
+ * Notification closed state.
+ */
+ closed: boolean;
+}>(undefined);
+
+/**
+ * Notification component.
+ */
+function NotificationInstance({uuid, children}: React.PropsWithChildren<{
+ /**
+ * Notification UUID.
+ */
+ uuid: NotificationUuid;
+}>)
+{
+ // Get notifications context.
+ const {isClosed} = useContext(NotificationsContext);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/styles/_components.less b/src/styles/_components.less
index 4b51633..3eb9f1b 100644
--- a/src/styles/_components.less
+++ b/src/styles/_components.less
@@ -12,6 +12,7 @@
@import "components/_loaders";
@import "components/_menus";
@import "components/_modal";
+@import "components/_notifications";
@import "components/_pagination";
@import "components/_select";
@import "components/_steps";
diff --git a/src/styles/components/_curtains.less b/src/styles/components/_curtains.less
index cc37f71..b05f19c 100644
--- a/src/styles/components/_curtains.less
+++ b/src/styles/components/_curtains.less
@@ -31,7 +31,7 @@ body.dimmed
{ // Disable scroll and blur the content when the body is dimmed.
overflow: hidden;
- > *:not(.curtain)
+ > *:not(.curtain):not(.notifications)
{
transition: filter 0.4s ease-in;
diff --git a/src/styles/components/_notifications.less b/src/styles/components/_notifications.less
new file mode 100644
index 0000000..7dc9697
--- /dev/null
+++ b/src/styles/components/_notifications.less
@@ -0,0 +1,86 @@
+body > ul.notifications
+{ // Notifications list.
+ position: fixed;
+ top: 0;
+ right: 0;
+
+ margin: 0 1em 1em;
+ padding: 0;
+ width: 20em;
+ max-width: 66%;
+
+ list-style: none;
+
+ z-index: 2000;
+
+ > li.notification
+ { // A single notification.
+ margin: 1em auto;
+ padding: 1em;
+
+ border-radius: 0.25em;
+
+ box-shadow: 0 0 0.5em 0 var(--foreground-shadow);
+ background: var(--background);
+
+ // Show an animation when entering screen.
+ animation: notification-in 0.3s ease-in;
+ transform-origin: center;
+
+ &.closed
+ { // Added when the notification is closing and will soon be removed from DOM.
+ transition: transform 0.3s ease-out, filter 0.3s ease-out, opacity 0.3s ease-out;
+
+ transform: scale(1.15);
+ filter: blur(0.25em);
+ opacity: 0;
+
+ pointer-events: none;
+ }
+
+ &.info
+ {
+ border: solid var(--primary-darker) thin;
+ background: var(--primary);
+ color: var(--background);
+ }
+
+ &.success
+ {
+ border: solid var(--green-darker) thin;
+ background: var(--green);
+ color: var(--background);
+ }
+
+ &.warning
+ {
+ border: solid var(--orange-darker) thin;
+ background: var(--orange);
+ color: var(--background);
+ }
+
+ &.error
+ {
+ border: solid var(--red-darker) thin;
+ background: var(--red);
+ color: var(--background);
+ }
+ }
+}
+
+@keyframes notification-in
+{ // Screen enter animation.
+ from
+ {
+ transform: scale(1.15);
+ filter: blur(0.25em);
+ opacity: 0;
+ }
+
+ to
+ {
+ transform: scale(1);
+ filter: blur(0);
+ opacity: 1;
+ }
+}