Procházet zdrojové kódy

Feature: Portalize system added.
Feature: Toast component added.
Feature: Toast system added.

lfabl před 1 měsícem
rodič
revize
4ba9ccffe1

+ 0 - 1
example/package.json

@@ -22,7 +22,6 @@
         "react": "19.2.0",
         "react-dom": "19.2.0",
         "react-native": "0.83.2",
-        "react-native-portalize": "^1.0.7",
         "react-native-safe-area-context": "^5.7.0",
         "react-native-screens": "^4.24.0",
         "react-native-svg": "^15.15.3",

+ 33 - 4
example/src/pages/home/index.tsx

@@ -1,6 +1,6 @@
 import {
-    useRef,
-    useState
+    useState,
+    useRef
 } from "react";
 import {
     View
@@ -9,13 +9,14 @@ import stylesheet from "./stylesheet";
 import {
     type IBottomSheetRef,
     getNCoreUIKitVersion,
+    NCoreUIKitToast,
     NCoreUIKitTheme,
     BottomSheet,
     TextInput,
     SelectBox,
+    CheckBox,
     Button,
-    Text,
-    CheckBox
+    Text
 } from "ncore-ui-kit-mobile";
 import {
     useNavigation
@@ -24,6 +25,9 @@ import type {
     NativeStackNavigationProp
 } from "@react-navigation/native-stack";
 import type RootStackParamList from "../../navigation/type";
+import {
+    HomeIcon
+} from "lucide-react-native";
 
 const X = [
     {
@@ -264,6 +268,21 @@ const Home = () => {
 
             }}
         />
+        <Button
+            title="Open Toast"
+            variant="filled"
+            type="warning"
+            onPress={() => {
+                NCoreUIKitToast.open({
+                    title: "sdgfsdgsdafs sdlşlgkdfşiklh",
+                    subTitle: "tr9uıdgfss 0dgklsd",
+                    isShowAction: false,
+                    icon: ({
+                        color
+                    }) => <HomeIcon color={colors.content.icon[color ? color : "default"]}/>
+                });
+            }}
+        />
         <BottomSheet
             ref={bottomSheetRef}
             renderHeader={() => {
@@ -282,6 +301,16 @@ const Home = () => {
             snapPoint={300}
             key="ahmet"
         >
+            <Button
+                title="Open Toast"
+                variant="filled"
+                type="warning"
+                onPress={() => {
+                    NCoreUIKitToast.open({
+                        title: "sdgfsdgsdg"
+                    });
+                }}
+            />
             <Button
                 onPress={() => {
                     navigation.navigate("TestSubPage");

+ 0 - 2
package.json

@@ -102,7 +102,6 @@
         "ncore-context": ">= 1.0.5",
         "react": "*",
         "react-native": "*",
-        "react-native-portalize": ">= 1.0.7",
         "react-native-safe-area-context": ">= 5.7.0",
         "react-native-svg": ">= 15.15.3"
     },
@@ -158,7 +157,6 @@
     "dependencies": {
         "lucide-react-native": "0.577.0",
         "ncore-context": "1.0.5",
-        "react-native-portalize": "1.0.7",
         "react-native-safe-area-context": "5.7.0",
         "react-native-svg": "15.15.3"
     }

+ 2 - 0
src/components/button/index.tsx

@@ -30,6 +30,7 @@ import Text from "../text";
 const Button: FC<IButtonProps> = ({
     displayBehaviourWhileLoading = "disabled",
     spreadBehaviour = "baseline",
+    isCustomPadding = false,
     icon: IconComponentProp,
     iconDirection = "left",
     variant = "filled",
@@ -73,6 +74,7 @@ const Button: FC<IButtonProps> = ({
     } = useStyles({
         displayBehaviourWhileLoading,
         icon: IconComponentProp,
+        isCustomPadding,
         spreadBehaviour,
         currentVariant,
         iconDirection,

+ 8 - 0
src/components/button/stylesheet.ts

@@ -171,6 +171,7 @@ const stylesheet = StyleSheet.create({
 export const useStyles = ({
     displayBehaviourWhileLoading,
     spreadBehaviour,
+    isCustomPadding,
     currentVariant,
     iconDirection,
     currentType,
@@ -274,6 +275,13 @@ export const useStyles = ({
         }
     }
 
+    if(isCustomPadding) {
+        delete styles.container.paddingBottom;
+        delete styles.container.paddingRight;
+        delete styles.container.paddingLeft;
+        delete styles.container.paddingTop;
+    }
+
     return styles;
 };
 export default stylesheet;

+ 2 - 0
src/components/button/type.ts

@@ -18,6 +18,7 @@ export type ButtonDynamicStyleType = {
     currentVariant: ButtonVariants;
     borders: NCoreUIKit.Borders;
     currentSize: ButtonMeasures;
+    isCustomPadding?: boolean;
     currentType: ButtonTypes;
     variant: ButtonVariant;
     icon?: NCoreUIKitIcon;
@@ -89,6 +90,7 @@ interface IButtonProps extends Omit<ButtonProps, "title"> {
     };
     spreadBehaviour?: ButtonSpreadBehaviour;
     iconDirection?: "left" | "right";
+    isCustomPadding?: boolean;
     variant?: ButtonVariant;
     icon?: NCoreUIKitIcon;
     isDisabled?: boolean;

+ 4 - 0
src/components/index.ts

@@ -54,3 +54,7 @@ export {
 export {
     default as CheckBox
 } from "./checkBox";
+
+export {
+    default as Toast
+} from "./toast";

+ 2 - 2
src/components/modal/index.tsx

@@ -24,7 +24,7 @@ import {
 } from "../../types";
 import {
     Portal
-} from "react-native-portalize";
+} from "../../helpers/portalize";
 
 /**
  * A generic modal
@@ -170,7 +170,7 @@ const Modal: RefForwardingComponent<IModalRef, IModalProps> = ({
     };
 
     const renderWithPortal = () => {
-        return <Portal>
+        return <Portal name="modal-system">
             {renderContainer()}
         </Portal>;
     };

+ 4 - 4
src/components/modal/stylesheet.ts

@@ -6,7 +6,7 @@ const stylesheet = StyleSheet.create({
     container: {
         position: "absolute",
         display: "flex",
-        zIndex: 99997,
+        zIndex: 99994,
         bottom: 0,
         right: 0,
         left: 0,
@@ -14,7 +14,7 @@ const stylesheet = StyleSheet.create({
     },
     overlay: {
         position: "absolute",
-        zIndex: 99998,
+        zIndex: 99995,
         bottom: 0,
         right: 0,
         left: 0,
@@ -22,7 +22,7 @@ const stylesheet = StyleSheet.create({
     },
     overlayContent: {
         position: "absolute",
-        zIndex: 99998,
+        zIndex: 99996,
         bottom: 0,
         right: 0,
         left: 0,
@@ -30,7 +30,7 @@ const stylesheet = StyleSheet.create({
     },
     content: {
         display: "contents",
-        zIndex: 99999
+        zIndex: 99997
     }
 });
 export default stylesheet;

+ 273 - 0
src/components/toast/index.tsx

@@ -0,0 +1,273 @@
+import {
+    useEffect,
+    type FC,
+    useRef
+} from "react";
+import {
+    TouchableWithoutFeedback,
+    Animated,
+    Easing,
+    View
+} from "react-native";
+import type IToastProps from "./type";
+import stylesheet, {
+    getToastType,
+    useStyles
+} from "./stylesheet";
+import {
+    NCoreUIKitTheme
+} from "../../core/hooks";
+import {
+    useSafeAreaInsets
+} from "react-native-safe-area-context";
+import {
+    X as XIcon
+} from "lucide-react-native";
+import {
+    Portal
+} from "../../helpers/portalize";
+import Button from "../button";
+import Text from "../text";
+
+const Toast: FC<IToastProps> = ({
+    isCloseOnPressActionButton = true,
+    closeAnimationDelay = 350,
+    openAnimationDelay = 200,
+    contentContainerStyle,
+    autoCloseDelay = 3000,
+    isCloseOnPress = true,
+    icon: CustomIcon,
+    type = "neutral",
+    isShowAction,
+    customTheme,
+    onClosed,
+    subTitle,
+    children,
+    action,
+    style,
+    title,
+    id
+}) => {
+    const {
+        radiuses,
+        colors,
+        spaces
+    } = NCoreUIKitTheme.useContext(customTheme);
+
+    const {
+        bottom
+    } = useSafeAreaInsets();
+
+    const TRANSFORM_DISTANCE = 75;
+
+    const transformAnim = useRef(new Animated.Value(TRANSFORM_DISTANCE)).current;
+    const opacityAnim = useRef(new Animated.Value(0)).current;
+
+    const currentType = getToastType({
+        type
+    });
+
+    const {
+        contentContainer: contentContainerDynamicStyle,
+        containerObject: containerObjectDynamicStyle,
+        iconContainer: iconContainerDynamicStyle,
+        container: containerDynamicStyle,
+        subTitle: subTitleDynamicStyle,
+        action: actionDynamicStyle,
+        title: titleDynamicStyle
+    } = useStyles({
+        safeAreaBottom: bottom,
+        currentType,
+        radiuses,
+        subTitle,
+        spaces,
+        colors,
+        type
+    });
+
+    useEffect(() => {
+        Animated.parallel([
+            Animated.timing(opacityAnim, {
+                duration: openAnimationDelay,
+                useNativeDriver: true,
+                easing: Easing.linear,
+                toValue: 1
+            }),
+            Animated.timing(transformAnim, {
+                duration: openAnimationDelay,
+                useNativeDriver: true,
+                easing: Easing.linear,
+                toValue: 0
+            })
+        ]).start();
+
+        setTimeout(() => {
+            closeAnimation();
+        }, autoCloseDelay);
+    }, []);
+
+    const closeAnimation = (_onClosed?: (props: {
+        id: string;
+    }) => void) => {
+        Animated.parallel([
+            Animated.timing(transformAnim, {
+                duration: closeAnimationDelay,
+                toValue: TRANSFORM_DISTANCE,
+                useNativeDriver: true,
+                easing: Easing.linear
+            }),
+            Animated.timing(opacityAnim, {
+                duration: closeAnimationDelay,
+                useNativeDriver: true,
+                easing: Easing.linear,
+                toValue: 0
+            })
+        ]).start(({
+            finished
+        }) => {
+            if(finished) {
+                if(onClosed) onClosed({
+                    id
+                });
+
+                if(_onClosed) _onClosed({
+                    id
+                });
+            }
+        });
+    };
+
+    const renderIcon = () => {
+        if(!CustomIcon) {
+            return null;
+        }
+
+        return <View
+            style={[
+                stylesheet.iconContainer,
+                iconContainerDynamicStyle
+            ]}
+        >
+            <CustomIcon
+                color="default"
+                size={18}
+            />
+        </View>;
+    };
+
+    const renderContent = () => {
+        return <View
+            style={[
+                contentContainerStyle,
+                stylesheet.contentContainer,
+                contentContainerDynamicStyle
+            ]}
+        >
+            <Text
+                numberOfLines={3}
+                style={{
+                    ...stylesheet.title,
+                    ...titleDynamicStyle
+                }}
+            >
+                {title}
+            </Text>
+            {
+                subTitle ?
+                    <Text
+                        variant="labelSmallSize"
+                        numberOfLines={2}
+                        color="low"
+                        style={{
+                            ...stylesheet.subTitle,
+                            ...subTitleDynamicStyle
+                        }}
+                    >
+                        {subTitle}
+                    </Text>
+                    :
+                    null
+            }
+        </View>;
+    };
+
+    const renderAction = () => {
+        if(!isShowAction) {
+            return null;
+        }
+
+        return <Button
+            title={action && action.title ? action.title : undefined}
+            isCustomPadding={true}
+            spreadBehaviour="free"
+            onPress={() => {
+                if(isCloseOnPressActionButton) closeAnimation();
+
+                if(action && action.onPress) action.onPress({
+                    closeAnimation: ({
+                        onClosed: _onClosed
+                    }) => closeAnimation(_onClosed)
+                });
+            }}
+            icon={({
+                color
+            }) => {
+                if(action?.title) {
+                    return null;
+                }
+
+                return <XIcon
+                    color={colors.content.icon[color ? color : "default"]}
+                    size={18}
+                />;
+            }}
+            style={{
+                ...action?.style,
+                ...actionDynamicStyle
+            }}
+            variant="ghost"
+            type="neutral"
+            size="small"
+        />;
+    };
+
+    const renderContainer = () => {
+        return <View
+            style={[
+                stylesheet.containerObject,
+                containerObjectDynamicStyle
+            ]}
+        >
+            {renderIcon()}
+            {renderContent()}
+            {renderAction()}
+        </View>;
+    };
+
+    return <Portal name="toast-system">
+        <Animated.View
+            style={[
+                style,
+                stylesheet.container,
+                containerDynamicStyle,
+                {
+                    opacity: opacityAnim,
+                    transform: [{
+                        translateY: transformAnim
+                    }]
+                }
+            ]}
+        >
+            <TouchableWithoutFeedback
+                onPress={() => {
+                    if(isCloseOnPress) {
+                        closeAnimation();
+                    }
+                }}
+            >
+                {children ? children : renderContainer()}
+            </TouchableWithoutFeedback>
+        </Animated.View>
+    </Portal>;
+};
+export default Toast;

+ 156 - 0
src/components/toast/stylesheet.ts

@@ -0,0 +1,156 @@
+import {
+    StyleSheet,
+    type TextStyle,
+    type ViewStyle
+} from "react-native";
+import type {
+    ToastDynamicStyleType,
+    ToastType,
+    ToastTypeConstantType,
+    ToastTypes
+} from "./type";
+import type {
+    Mutable
+} from "../../types";
+import {
+    windowWidth
+} from "../../utils";
+
+export const CHECK_BOX_TYPE_STYLES: Record<
+    ToastType,
+    {
+        containerColor: keyof NCoreUIKit.ContainerContentColors;
+        subTitleColor: keyof NCoreUIKit.TextContentColors;
+        actionColor: keyof NCoreUIKit.TextContentColors;
+        titleColor: keyof NCoreUIKit.TextContentColors;
+        iconColor: keyof NCoreUIKit.IconContentColors;
+    }
+> = {
+    primary: {
+        actionColor: "onPrimary",
+        iconColor: "emphasized",
+        containerColor: "mid",
+        subTitleColor: "low",
+        titleColor: "mid"
+    },
+    danger: {
+        subTitleColor: "dangerLow",
+        containerColor: "danger",
+        actionColor: "danger",
+        titleColor: "danger",
+        iconColor: "danger"
+    },
+    success: {
+        subTitleColor: "successLow",
+        containerColor: "success",
+        actionColor: "success",
+        titleColor: "success",
+        iconColor: "success"
+    },
+    warning: {
+        subTitleColor: "warningLow",
+        containerColor: "warning",
+        actionColor: "warning",
+        titleColor: "warning",
+        iconColor: "warning"
+    },
+    info: {
+        subTitleColor: "infoLow",
+        containerColor: "info",
+        actionColor: "info",
+        titleColor: "info",
+        iconColor: "info"
+    },
+    neutral: {
+        containerColor: "mid",
+        subTitleColor: "low",
+        actionColor: "mid",
+        titleColor: "mid",
+        iconColor: "mid"
+    }
+};
+
+export const getToastType = ({
+    type
+}: ToastTypeConstantType): ToastTypes => {
+    const currentType = CHECK_BOX_TYPE_STYLES[type];
+
+    return currentType;
+};
+
+const stylesheet = StyleSheet.create({
+    container: {
+        position: "absolute",
+        alignSelf: "center",
+        zIndex: 99998
+    },
+    containerObject: {
+        flexDirection: "row",
+        alignItems: "center"
+    },
+    contentContainer: {
+        justifyContent: "center",
+        alignItems: "flex-start",
+        flexDirection: "column",
+        flexShrink: 1
+    },
+    iconContainer: {
+        justifyContent: "center",
+        alignItems: "center"
+    },
+    title: {
+        textAlign: "left"
+    },
+    subTitle: {
+        textAlign: "left"
+    }
+});
+
+export const useStyles = ({
+    safeAreaBottom,
+    currentType,
+    radiuses,
+    subTitle,
+    colors,
+    spaces
+}: ToastDynamicStyleType) => {
+    const styles = {
+        container: {
+            backgroundColor: colors.content.container[currentType.containerColor],
+            maxWidth: windowWidth - (spaces.spacingMd * 2),
+            bottom: safeAreaBottom + 100,
+            borderRadius: radiuses.full
+        } as Mutable<ViewStyle>,
+        containerObject: {
+            paddingHorizontal: spaces.spacingLg,
+            paddingVertical: spaces.spacingMd
+        } as Mutable<ViewStyle>,
+        contentContainer: {
+
+        } as Mutable<ViewStyle>,
+        title: {
+
+        } as Mutable<TextStyle>,
+        subTitle: {
+            marginTop: spaces.spacingXs
+        } as Mutable<TextStyle>,
+        iconContainer: {
+            paddingRight: spaces.spacingSm,
+            marginRight: spaces.spacingSm
+        } as Mutable<ViewStyle>,
+        action: {
+            paddingBottom: spaces.spacingSm,
+            paddingRight: spaces.spacingSm,
+            paddingLeft: spaces.spacingMd,
+            paddingTop: spaces.spacingSm,
+            marginLeft: spaces.spacingMd
+        } as Mutable<ViewStyle>
+    };
+
+    if(subTitle) {
+        styles.container.borderRadius = radiuses.lg;
+    }
+
+    return styles;
+};
+export default stylesheet;

+ 80 - 0
src/components/toast/type.ts

@@ -0,0 +1,80 @@
+import {
+    type ReactNode
+} from "react";
+import {
+    type StyleProp,
+    type ViewStyle
+} from "react-native";
+import type {
+    NCoreUIKitIcon
+} from "../../types";
+
+export type ToastDynamicStyleType = {
+    radiuses: NCoreUIKit.ActivePalette["radiuses"];
+    spaces: NCoreUIKit.ActivePalette["spaces"];
+    colors: NCoreUIKit.ActivePalette["colors"];
+    currentType: ToastTypes;
+    safeAreaBottom: number;
+    subTitle?: string;
+    theme?: undefined;
+    type: ToastType;
+};
+
+export type ToastTypes = {
+    containerColor: keyof NCoreUIKit.ContainerContentColors;
+    subTitleColor: keyof NCoreUIKit.TextContentColors;
+    actionColor: keyof NCoreUIKit.TextContentColors;
+    titleColor: keyof NCoreUIKit.TextContentColors;
+    iconColor: keyof NCoreUIKit.IconContentColors;
+};
+
+export type ToastTypeConstantType = {
+    type: ToastType;
+};
+
+export type ToastType = "primary" | "danger" | "warning" | "neutral" | "success" | "info";
+
+export type ToastInternalProps = {
+    contentContainerStyle?: StyleProp<ViewStyle>[] | StyleProp<ViewStyle>;
+    style?: StyleProp<ViewStyle>[] | StyleProp<ViewStyle>;
+    action?: {
+        onPress: (props: {
+            closeAnimation: (props: {
+                onClosed?: (props: {
+                    id: string;
+                }) => void;
+            }) => void;
+        }) => void;
+        icon?: NCoreUIKitIcon;
+        style?: ViewStyle;
+        title?: string;
+    };
+    isCloseOnPressActionButton?: boolean;
+    closeAnimationDelay?: number;
+    openAnimationDelay?: number;
+    isCloseOnPress?: boolean;
+    autoCloseDelay?: number;
+    isShowAction?: boolean;
+    icon?: NCoreUIKitIcon;
+    children?: ReactNode;
+    subTitle?: string;
+    type?: ToastType;
+    title: string;
+    id: string;
+    onClosed?: (props: {
+        id: string;
+    }) => void;
+    customTheme?: {
+        gapPropagation?: NCoreUIKit.GapPropagationKey;
+        sharpness?: NCoreUIKit.SharpnessKey;
+        paletteKey?: NCoreUIKit.PaletteKey;
+        themeKey?: NCoreUIKit.ThemeKey;
+    };
+};
+
+interface IToastProps extends ToastInternalProps {
+};
+
+export type {
+    IToastProps as default
+};

+ 23 - 5
src/context/index.tsx

@@ -2,16 +2,21 @@ import {
     type ReactNode
 } from "react";
 import NCoreUIKitLocalize from "./localize";
+import NCoreUIKitToast from "./toast";
 import NCoreUIKitModal from "./modal";
 import NCoreUIKitTheme from "./theme";
 import {
     type NCoreUIKitConfig
 } from "../types";
+import {
+    Host
+} from "../helpers/portalize";
 
 class CoreContext<T extends NCoreUIKitConfig> {
     NCoreUIKitLocalize: NCoreUIKitLocalize<T>;
     NCoreUIKitTheme: NCoreUIKitTheme<T>;
     NCoreUIKitModal: NCoreUIKitModal;
+    NCoreUIKitToast: NCoreUIKitToast;
 
     constructor(initialState: T) {
         this.NCoreUIKitTheme = new NCoreUIKitTheme({
@@ -30,6 +35,10 @@ class CoreContext<T extends NCoreUIKitConfig> {
         this.NCoreUIKitModal = new NCoreUIKitModal({
             data: []
         });
+
+        this.NCoreUIKitToast = new NCoreUIKitToast({
+            data: []
+        });
     }
 
     Provider = ({
@@ -39,15 +48,24 @@ class CoreContext<T extends NCoreUIKitConfig> {
     }) => {
         const LocalizeContext = this.NCoreUIKitLocalize;
         const ModalContext = this.NCoreUIKitModal;
+        const ToastContext = this.NCoreUIKitToast;
         const ThemeContext = this.NCoreUIKitTheme;
 
         return <ThemeContext.Provider>
             <LocalizeContext.Provider>
-                <ModalContext.Provider>
-                    <ModalContext.Render>
-                        {children}
-                    </ModalContext.Render>
-                </ModalContext.Provider>
+                <Host name="toast-system">
+                    <ModalContext.Provider>
+                        <ModalContext.Render>
+                            <Host name="modal-system">
+                                <ToastContext.Provider>
+                                    <ToastContext.Render>
+                                        {children}
+                                    </ToastContext.Render>
+                                </ToastContext.Provider>
+                            </Host>
+                        </ModalContext.Render>
+                    </ModalContext.Provider>
+                </Host>
             </LocalizeContext.Provider>
         </ThemeContext.Provider>;
     };

+ 12 - 17
src/context/modal.tsx

@@ -9,9 +9,6 @@ import {
 import NCoreContext, {
     type ConfigType
 } from "ncore-context";
-import {
-    Host
-} from "react-native-portalize";
 import Modal from "../components/modal";
 
 class NCoreUIKitModal extends NCoreContext<ModalContextType, ConfigType<ModalContextType>> {
@@ -86,20 +83,18 @@ class NCoreUIKitModal extends NCoreContext<ModalContextType, ConfigType<ModalCon
         } = this.useContext();
 
         return <Fragment>
-            <Host>
-                {children}
-                {data.map((item: ModalDataType) => {
-                    return <Modal
-                        key={`NCoreUIKit-Modal-${item.id}`}
-                        onOverlayPress={item.onOverlayPress ? item.onOverlayPress : () => {
-                            this.close({
-                                id: item.id
-                            });
-                        }}
-                        {...item}
-                    />;
-                })}
-            </Host>
+            {children}
+            {data.map((item: ModalDataType) => {
+                return <Modal
+                    key={`NCoreUIKit-Modal-${item.id}`}
+                    onOverlayPress={item.onOverlayPress ? item.onOverlayPress : () => {
+                        this.close({
+                            id: item.id
+                        });
+                    }}
+                    {...item}
+                />;
+            })}
         </Fragment>;
     };
 }

+ 114 - 0
src/context/toast.tsx

@@ -0,0 +1,114 @@
+import {
+    type ReactNode,
+    Fragment
+} from "react";
+import {
+    type ToastContextType,
+    type ToastDataType
+} from "../types/toast";
+import NCoreContext, {
+    type ConfigType
+} from "ncore-context";
+import Toast from "../components/toast";
+import {
+    uuid
+} from "../utils";
+
+class NCoreUIKitToast extends NCoreContext<ToastContextType, ConfigType<ToastContextType>> {
+    constructor({
+        data = []
+    }: {
+        data?: Array<ToastDataType>
+    }) {
+        super({
+            close: () => {},
+            open: () => "",
+            data: data
+        }, {
+            key: "NCoreUIKit-ToastContext"
+        });
+    };
+
+    open = (toastData: ToastDataType) => {
+        const currentData = this.state.data;
+
+        const toastID = toastData.id ? toastData.id : uuid();
+
+        currentData.push({
+            ...toastData,
+            id: toastID
+        });
+
+        this.setState({
+            data: currentData
+        });
+
+        return toastID;
+    };
+
+    close = (props?: {
+        index?: number;
+        id?: string;
+    }) => {
+        const currentData = this.state.data;
+
+        if (props && props.id) {
+            const keyIndex = currentData.findIndex((toast) => toast.id === props.id);
+
+            if (keyIndex !== -1) {
+                currentData.splice(keyIndex, 1);
+
+                this.setState({
+                    data: currentData
+                });
+            }
+
+            return;
+        }
+
+        if (props && props.index !== undefined) {
+            currentData.splice(props.index, 1);
+
+            this.setState({
+                data: currentData
+            });
+
+            return;
+        }
+
+        currentData.pop();
+
+        this.setState({
+            data: currentData
+        });
+    };
+
+    Render = ({
+        children
+    }: {
+        children: ReactNode;
+    }) => {
+        const {
+            data
+        } = this.useContext();
+
+        return <Fragment>
+            {children}
+            {data.map((item: ToastDataType) => {
+                return <Toast
+                    key={`NCoreUIKit-Toast-${item.id}`}
+                    {...item}
+                    id={item.id as string}
+                    onClosed={(props) => {
+                        this.close({
+                            id: item.id
+                        });
+
+                        if(item.onClosed) item.onClosed(props);
+                    }}
+                />;
+            })}
+        </Fragment>;
+    };
+}
+export default NCoreUIKitToast;

+ 3 - 0
src/core/hooks.ts

@@ -8,14 +8,17 @@ import type {
 } from "../types";
 import type NCoreUIKitLocalizeClass from "../context/localize";
 import type NCoreUIKitModalClass from "../context/modal";
+import type NCoreUIKitToastClass from "../context/toast";
 import type NCoreUIKitThemeClass from "../context/theme";
 
 export let NCoreUIKitLocalize: NCoreUIKitLocalizeClass<LocalizeType>;
 export let NCoreUIKitTheme: NCoreUIKitThemeClass<ThemesType>;
 export let NCoreUIKitModal: NCoreUIKitModalClass;
+export let NCoreUIKitToast: NCoreUIKitToastClass;
 
 export const initializeInstances = (NCoreUIKit: NCoreUIKitBase<NCoreUIKitConfig>) => {
     NCoreUIKitLocalize = NCoreUIKit.NCoreUIKitContext.NCoreUIKitLocalize;
     NCoreUIKitTheme = NCoreUIKit.NCoreUIKitContext.NCoreUIKitTheme;
     NCoreUIKitModal = NCoreUIKit.NCoreUIKitContext.NCoreUIKitModal;
+    NCoreUIKitToast = NCoreUIKit.NCoreUIKitContext.NCoreUIKitToast;
 };

+ 4 - 4
src/core/index.tsx

@@ -29,11 +29,11 @@ export class NCoreUIKitBase<T extends NCoreUIKitConfig> {
     }) => {
         const NCoreUIKitContext = this.NCoreUIKitContext;
 
-        return <NCoreUIKitContext.Provider>
-            <SafeAreaProvider>
+        return <SafeAreaProvider>
+            <NCoreUIKitContext.Provider>
                 {children}
-            </SafeAreaProvider>
-        </NCoreUIKitContext.Provider>;
+            </NCoreUIKitContext.Provider>;
+        </SafeAreaProvider>;
     };
 };
 

+ 53 - 0
src/helpers/portalize/Consumer.tsx

@@ -0,0 +1,53 @@
+import {
+    type ReactNode,
+    useEffect,
+    useRef
+} from "react";
+import {
+    type IProvider
+} from "./Host";
+
+interface IConsumerProps {
+    manager: IProvider | null;
+    children: ReactNode;
+    name?: string;
+};
+
+export const Consumer = ({
+    children,
+    manager,
+    name
+}: IConsumerProps): null => {
+    const key = useRef<string | undefined>(undefined);
+
+    const checkManager = (): void => {
+        if (!manager) {
+            throw new Error("No portal manager defined");
+        }
+    };
+
+    const handleInit = (): void => {
+        checkManager();
+        key.current = manager?.mount(children, name);
+    };
+
+    useEffect(() => {
+        checkManager();
+        manager?.update(key.current, children, name);
+    }, [
+        children,
+        manager,
+        name
+    ]);
+
+    useEffect(() => {
+        handleInit();
+
+        return (): void => {
+            checkManager();
+            manager?.unmount(key.current);
+        };
+    }, []);
+
+    return null;
+};

+ 122 - 0
src/helpers/portalize/Host.tsx

@@ -0,0 +1,122 @@
+import {
+    type ReactNode,
+    createContext,
+    useEffect,
+    useRef
+} from "react";
+import {
+    type ViewStyle,
+    View
+} from "react-native";
+import {
+    useKey
+} from "./hooks/useKey";
+import {
+    type IManagerHandles,
+    Manager
+} from "./Manager";
+import {
+    context
+} from "./context";
+
+interface IHostProps {
+    children: ReactNode;
+    style?: ViewStyle;
+    name?: string;
+}
+
+export interface IProvider {
+    update(key?: string, children?: ReactNode, name?: string): void;
+    mount(children: ReactNode, name?: string): string;
+    unmount(key?: string): void;
+    name?: string;
+}
+
+export const Context = createContext<IProvider | null>(null);
+
+export const Host = ({
+    children,
+    style,
+    name
+}: IHostProps): ReactNode => {
+    const managerRef = useRef<IManagerHandles>(null);
+
+    const queue: Array<{
+        type: "mount" | "update" | "unmount";
+        children?: ReactNode;
+        name?: string;
+        key: string;
+    }> = [];
+
+    const {
+        generateKey,
+        removeKey
+    } = useKey();
+
+    useEffect(() => {
+        while (queue.length && managerRef.current) {
+            const action = queue.pop();
+
+            if (action) {
+                switch (action.type) {
+                    case "mount":
+                        managerRef.current?.mount(action.key, action.children, action.name);
+                        break;
+                    case "update":
+                        managerRef.current?.update(action.key, action.children, action.name);
+                        break;
+                    case "unmount":
+                        managerRef.current?.unmount(action.key);
+                        break;
+                }
+            }
+        }
+    }, []);
+
+    const mount = (children: ReactNode, _name?: string): string => {
+        const key = generateKey();
+
+        const targetName = _name ?? name;
+
+        context.mount(key, children, targetName);
+
+        return key;
+    };
+
+    const update = (key: string, children: ReactNode, _name?: string): void => {
+        const targetName = _name ?? name;
+
+        context.update(key, children, targetName);
+    };
+
+    const unmount = (key: string): void => {
+        context.unmount(key);
+
+        removeKey(key);
+    };
+
+    return <Context.Provider value={{
+        unmount,
+        update,
+        mount,
+        name
+    }}>
+        <View
+            style={[
+                {
+                    flex: 1
+                },
+                style
+            ]}
+            collapsable={false}
+            pointerEvents="box-none"
+        >
+            {children}
+        </View>
+
+        <Manager
+            ref={managerRef}
+            name={name}
+        />
+    </Context.Provider>;
+};

+ 75 - 0
src/helpers/portalize/Manager.tsx

@@ -0,0 +1,75 @@
+import {
+    useImperativeHandle,
+    type ReactNode,
+    forwardRef,
+    useState,
+    useEffect
+} from "react";
+import {
+    StyleSheet,
+    View
+} from "react-native";
+import {
+    context
+} from "./context";
+
+export interface IManagerHandles {
+    update(key?: string, children?: ReactNode, name?: string): void;
+    mount(key: string, children: ReactNode, name?: string): void;
+    unmount(key?: string): void;
+}
+
+export const Manager = forwardRef(({
+    name
+}: {
+    name?: string;
+}, ref): Array<ReactNode> => {
+    const [
+        portals,
+        setPortals
+    ] = useState<Array<{
+        children: ReactNode;
+        name?: string;
+        key: string;
+    }>>([]);
+
+    useEffect(() => {
+        return context.subscribe((newPortals) => {
+            setPortals(newPortals);
+        });
+    }, []);
+
+    useImperativeHandle(
+        ref,
+        (): IManagerHandles => ({
+            unmount: context.unmount,
+            update: context.update,
+            mount: context.mount
+        }),
+    );
+
+    return portals
+        .filter(item => {
+            if (item.name) {
+                return item.name === name;
+            }
+
+            return !name;
+        })
+        .map((
+            {
+                children,
+                key
+            },
+            index: number
+        ) => (
+            <View
+                key={`NCoreUIKit-Portal-${key}-${index}`}
+                style={StyleSheet.absoluteFill}
+                pointerEvents="box-none"
+                collapsable={false}
+            >
+                {children}
+            </View>
+        ));
+});

+ 30 - 0
src/helpers/portalize/Portal.tsx

@@ -0,0 +1,30 @@
+import {
+    type ReactNode
+} from "react";
+import {
+    Consumer
+} from "./Consumer";
+import {
+    Context
+} from "./Host";
+
+interface IPortalProps {
+    children: ReactNode;
+    name?: string;
+}
+
+export const Portal = ({
+    children,
+    name
+}: IPortalProps): ReactNode => (
+    <Context.Consumer>
+        {(manager): ReactNode => {
+            return <Consumer
+                manager={manager}
+                name={name}
+            >
+                {children}
+            </Consumer>;
+        }}
+    </Context.Consumer>
+);

+ 49 - 0
src/helpers/portalize/context/index.ts

@@ -0,0 +1,49 @@
+import type {
+    ReactNode
+} from "react";
+
+type PortalItem = {
+    children: ReactNode;
+    name?: string;
+    key: string;
+};
+
+let portals: PortalItem[] = [];
+
+const listeners: Array<(portals: PortalItem[]) => void> = [];
+
+export const context = {
+    subscribe: (listener: (portals: PortalItem[]) => void) => {
+        listeners.push(listener);
+
+        return () => {
+            listeners.splice(listeners.indexOf(listener), 1);
+        };
+    },
+    mount: (key: string, children: ReactNode, name?: string) => {
+        portals = [
+            ...portals,
+            {
+                children,
+                name,
+                key
+            }
+        ];
+
+        listeners.forEach(l => l(portals));
+    },
+    update: (key: string, children: ReactNode, name?: string) => {
+        portals = portals.map(p => p.key === key ? {
+            ...p,
+            children,
+            name
+        } : p);
+
+        listeners.forEach(l => l(portals));
+    },
+    unmount: (key: string) => {
+        portals = portals.filter(p => p.key !== key);
+
+        listeners.forEach(l => l(portals));
+    }
+};

+ 49 - 0
src/helpers/portalize/hooks/useKey.ts

@@ -0,0 +1,49 @@
+import {
+    useRef
+} from "react";
+
+interface IUseKey {
+    removeKey(key: string): void;
+    generateKey(): string;
+}
+
+const keyGenerator = (): string => {
+    return `portalize_${Math.random().toString(36).substr(2, 16)}-${Math.random()
+        .toString(36)
+        .substr(2, 16)}-${Math.random().toString(36).substr(2, 16)}`;
+};
+
+export const useKey = (): IUseKey => {
+    const usedKeys = useRef<Array<string>>([]);
+
+    const generateKey = (): string => {
+        let foundUniqueKey = false;
+        let newKey = "";
+        let tries = 0;
+
+        while (!foundUniqueKey && tries < 3) {
+            tries++;
+            newKey = keyGenerator();
+
+            if (!usedKeys.current.includes(newKey)) {
+                foundUniqueKey = true;
+            }
+        }
+
+        if (!foundUniqueKey) {
+            newKey = `portalize_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
+        }
+
+        usedKeys.current.push(newKey);
+        return newKey;
+    };
+
+    const removeKey = (key: string): void => {
+        usedKeys.current = usedKeys.current.filter(k => k !== key);
+    };
+
+    return {
+        generateKey,
+        removeKey
+    };
+};

+ 15 - 0
src/helpers/portalize/index.tsx

@@ -0,0 +1,15 @@
+export {
+    Consumer
+} from "./Consumer";
+
+export {
+    Host
+} from "./Host";
+
+export {
+    Manager
+} from "./Manager";
+
+export {
+    Portal
+} from "./Portal";

+ 10 - 2
src/index.tsx

@@ -6,7 +6,8 @@ export {
 export {
     NCoreUIKitLocalize,
     NCoreUIKitModal,
-    NCoreUIKitTheme
+    NCoreUIKitTheme,
+    NCoreUIKitToast
 } from "./core/hooks";
 
 export {
@@ -20,6 +21,7 @@ export {
     // Dialog,
     Button,
     Modal,
+    Toast,
     Text
 } from "./components";
 
@@ -49,5 +51,11 @@ export type {
     ThemesType,
     LocaleType,
     ThemeType,
-    ModalType
+    ModalType,
+    Mutable
 } from "./types";
+
+export {
+    Portal,
+    Host
+} from "./helpers/portalize";

+ 24 - 0
src/types/toast.ts

@@ -0,0 +1,24 @@
+import {
+    type ToastInternalProps
+} from "../components/toast/type";
+
+export type ToastType = {
+    data?: Array<ToastDataType>;
+};
+
+export type ToastDataType = Omit<ToastInternalProps, "id"> & {
+    id?: string;
+};
+
+export type ToastContextType = {
+    open: (toastData: ToastDataType) => string;
+    close: (props?: {
+        index?: number;
+        id?: string;
+    }) => void;
+    data: Array<ToastDataType>;
+};
+
+export type ToastStateContextType = {
+    data: Array<ToastDataType>;
+};

+ 0 - 13
yarn.lock

@@ -13187,7 +13187,6 @@ __metadata:
     react-native: "npm:0.83.2"
     react-native-builder-bob: "npm:0.40.13"
     react-native-monorepo-config: "npm:0.3.3"
-    react-native-portalize: "npm:^1.0.7"
     react-native-safe-area-context: "npm:^5.7.0"
     react-native-screens: "npm:^4.24.0"
     react-native-svg: "npm:^15.15.3"
@@ -13224,7 +13223,6 @@ __metadata:
     react: "npm:19.2.0"
     react-native: "npm:0.83.2"
     react-native-builder-bob: "npm:0.40.13"
-    react-native-portalize: "npm:1.0.7"
     react-native-safe-area-context: "npm:5.7.0"
     react-native-svg: "npm:15.15.3"
     release-it: "npm:19.0.4"
@@ -13236,7 +13234,6 @@ __metadata:
     ncore-context: ">= 1.0.5"
     react: "*"
     react-native: "*"
-    react-native-portalize: ">= 1.0.7"
     react-native-safe-area-context: ">= 5.7.0"
     react-native-svg: ">= 15.15.3"
   languageName: unknown
@@ -14787,16 +14784,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-native-portalize@npm:1.0.7, react-native-portalize@npm:^1.0.7":
-  version: 1.0.7
-  resolution: "react-native-portalize@npm:1.0.7"
-  peerDependencies:
-    react: "> 15.0.0"
-    react-native: "> 0.50.0"
-  checksum: 10c0/17ba66a92e5d73b9341a5a748219801ae1ebe110003fc640bb44df8661f579a292b8d5521b0b481ee1281ed66d9d554398d42bb4e84827137455ab980d143aec
-  languageName: node
-  linkType: hard
-
 "react-native-safe-area-context@npm:5.7.0, react-native-safe-area-context@npm:^5.7.0":
   version: 5.7.0
   resolution: "react-native-safe-area-context@npm:5.7.0"