Sfoglia il codice sorgente

Feature: React-Native Web support added.

lfabl 1 settimana fa
parent
commit
baaf85bb36

+ 4 - 5
example/web/App.tsx

@@ -1,11 +1,10 @@
 import React from 'react';
-import { StyleSheet, View, Pressable } from 'react-native';
 import {
-  setupNCoreUIKit,
-  Button,
-  Text
+  StyleSheet
+} from 'react-native';
+import {
+  setupNCoreUIKit
 } from "ncore-ui-kit-mobile";
-import { EyeClosed } from 'lucide-react-native';
 import Navigation from './src/navigation';
 
 const NCoreUIKitBase = setupNCoreUIKit({

+ 328 - 12
src/components/bottomSheet/index.tsx

@@ -4,12 +4,14 @@ import {
     forwardRef,
     useEffect,
     useState,
-    useRef
+    useRef,
+    type PointerEvent
 } from "react";
 import {
     type LayoutChangeEvent,
     PanResponder,
     ScrollView,
+    Platform,
     Animated,
     Easing,
     View
@@ -25,15 +27,19 @@ import {
 import {
     useSafeAreaInsets
 } from "react-native-safe-area-context";
-import type {
-    RefForwardingComponent
+import {
+    safeGlobal,
+    webStyle,
+    type RefForwardingComponent,
+    type SafePointerEvent
 } from "../../types";
 import type {
     IModalRef
 } from "../modal/type";
 import {
     windowHeight,
-    uuid
+    uuid,
+    windowWidth
 } from "../../utils";
 import Modal from "../modal";
 
@@ -419,6 +425,7 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
                 return true;
             },
             onPanResponderGrant: (evt) => {
+                console.log("GRANT", evt.nativeEvent);
                 if(!isCanSwipeRef.current) return;
 
                 gestureStartY.current = evt.nativeEvent.pageY;
@@ -743,7 +750,274 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
     const renderView = () => {
         return <Animated.View
             {...props}
-            {...panResponder.panHandlers}
+            {...(Platform.OS === "web" ? {} : panResponder.panHandlers)}
+            onPointerDown={Platform.OS === "web" ? (evt) => {
+                const nativeEvt = evt as unknown as PointerEvent;
+                const startY = nativeEvt.clientY;
+                gestureStartY.current = nativeEvt.clientY;
+
+                const initialT = translateYValue.current;
+                const initialH = heightValue.current;
+                const initialS = scrollOffset.current;
+
+                animatedTranslateY.setOffset(initialT);
+                animatedTranslateY.setValue(0);
+
+                animatedHeight.setOffset(initialH);
+                animatedHeight.setValue(0);
+
+                let lastY = startY;
+                let lastTime = Date.now();
+                let velocityY = 0;
+
+                const handlePointerMove = (moveEvt: SafePointerEvent) => {
+                    if (!isCanSwipeRef.current) return;
+
+                    const dy = moveEvt.clientY - startY;
+                    if (Math.abs(dy) < 5) return;
+
+                    const now = Date.now();
+                    const dt = now - lastTime;
+                    if (dt > 0) {
+                        velocityY = (moveEvt.clientY - lastY) / dt;
+                    }
+                    lastY = moveEvt.clientY;
+                    lastTime = now;
+
+                    const delta = -dy;
+                    const pivot = snapPoint ?? contentHeight.current;
+                    const isAtTavan = heightValue.current >= maxHeight.current - 1;
+                    const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current;
+                    const isAtTop = scrollOffset.current <= 0;
+                    const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA;
+                    const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);
+
+                    if (dy > 0 && isAtTavan && hasScroll && !isAtTop && !isFromTopArea) {
+                        const usedForS = Math.max(-dy, -initialS);
+                        scrollOffset.current = initialS + usedForS;
+                        scrollViewRef.current?.scrollTo({
+                            y: scrollOffset.current,
+                            animated: false
+                        });
+                        return;
+                    }
+
+                    if (dy < 0) {
+                        let currentDelta = delta;
+
+                        const effectiveInitialT = Math.max(0, initialT);
+                        const usedForT = Math.min(currentDelta, effectiveInitialT);
+                        animatedTranslateY.setValue(-usedForT);
+                        currentDelta -= usedForT;
+
+                        if (currentDelta > 0) {
+                            if (initialH < pivot) {
+                                const spaceToPivot = pivot - initialH;
+                                const usedForH = Math.min(currentDelta, spaceToPivot);
+                                animatedHeight.setValue(usedForH);
+                                currentDelta -= usedForH;
+                            }
+
+                            const isRestoringHeight = initialH < pivot;
+                            const canScroll = isCanFullScreenOnSwipeRef.current || isWorkAsFullScreenRef.current || !isRestoringHeight;
+
+                            if (currentDelta > 0 && canScroll) {
+                                const remainingScroll = maxS - initialS;
+                                if (remainingScroll > 0) {
+                                    const usedForS = Math.min(currentDelta, remainingScroll);
+                                    scrollOffset.current = initialS + usedForS;
+                                    scrollViewRef.current?.scrollTo({
+                                        y: scrollOffset.current,
+                                        animated: false
+                                    });
+                                    currentDelta -= usedForS;
+                                    animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
+                                }
+                            }
+
+                            if (currentDelta > 0) {
+                                if (isCanFullScreenOnSwipeRef.current) {
+                                    const totalUsedBefore = initialH < pivot ? (pivot - initialH) : 0;
+                                    animatedHeight.setValue(totalUsedBefore + currentDelta);
+                                } else {
+                                    animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
+                                }
+                            }
+                        }
+                    } else {
+                        let currentDelta = delta;
+
+                        if (initialH > pivot || initialS > 0) {
+                            if (initialS > 0) {
+                                const usedForS = Math.max(currentDelta, -initialS);
+                                scrollOffset.current = initialS + usedForS;
+                                scrollViewRef.current?.scrollTo({
+                                    y: scrollOffset.current,
+                                    animated: false
+                                });
+                                currentDelta -= usedForS;
+                                animatedHeight.setValue(initialH > pivot ? 0 : pivot - initialH);
+                            }
+
+                            if (currentDelta < 0 && initialH > pivot) {
+                                const distanceToPivot = pivot - initialH;
+                                const usedForH = Math.max(currentDelta, distanceToPivot);
+                                animatedHeight.setValue(usedForH);
+                                currentDelta -= usedForH;
+                            }
+                        }
+
+                        if (currentDelta < 0) {
+                            animatedHeight.setValue(initialH > pivot ? (pivot - initialH) : 0);
+                            animatedTranslateY.setValue(-currentDelta);
+                            scrollOffset.current = 0;
+                            scrollViewRef.current?.scrollTo({
+                                y: 0,
+                                animated: false
+                            });
+                        }
+                    }
+                };
+
+                const handlePointerUp = () => {
+                    safeGlobal.removeEventListener("pointermove", handlePointerMove);
+                    safeGlobal.removeEventListener("pointerup", handlePointerUp);
+
+                    animatedHeight.flattenOffset();
+                    animatedTranslateY.flattenOffset();
+
+                    const currentT = translateYValue.current;
+                    const currentH = heightValue.current;
+                    const pivot = snapPoint ?? contentHeight.current;
+                    const tavan = maxHeight.current;
+
+                    const isAtTavan = currentH >= tavan - 1;
+                    const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current;
+                    const isAtTop = scrollOffset.current <= 1;
+                    const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA;
+
+                    if (isAtTavan && hasScroll && !isAtTop && !isFromTopArea) {
+                        if (Math.abs(velocityY) > 0.1) {
+                            const momentum = velocityY * -350;
+                            const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);
+                            const targetOffset = Math.max(0, Math.min(scrollOffset.current + momentum, maxS));
+
+                            const scrollAnim = new Animated.Value(scrollOffset.current);
+                            scrollAnim.addListener(({
+                                value
+                            }) => {
+                                scrollViewRef.current?.scrollTo({
+                                    y: value,
+                                    animated: false
+                                });
+                                scrollOffset.current = value;
+                            });
+
+                            Animated.timing(scrollAnim, {
+                                toValue: targetOffset,
+                                duration: Math.min(Math.abs(momentum) * 2, 600),
+                                easing: Easing.out(Easing.quad),
+                                useNativeDriver: false
+                            }).start(() => {
+                                scrollAnim.removeAllListeners();
+                                checkScrollThreshold();
+                            });
+                        } else {
+                            checkScrollThreshold();
+                        }
+                        return;
+                    }
+
+                    const isFastSwipeDown = velocityY > 0.5;
+                    const isFastSwipeUp = velocityY < -0.5;
+
+                    if (currentT <= Math.max(pivot * 0.2, 60)) {
+                        let toValue = pivot;
+
+                        if (isFastSwipeUp && isCanFullScreenOnSwipeRef.current) {
+                            toValue = tavan;
+                        } else if (isFastSwipeDown) {
+                            toValue = pivot;
+                        } else {
+                            if (!isCanFullScreenOnSwipeRef.current) {
+                                toValue = pivot;
+                            } else {
+                                const totalRange = tavan - pivot;
+                                const distFromPivot = currentH - pivot;
+
+                                if (distFromPivot < totalRange * 0.33) toValue = pivot;
+                                else if (distFromPivot > totalRange * 0.66) toValue = tavan;
+                                else toValue = (tavan - currentH) < (currentH - pivot) ? tavan : pivot;
+                            }
+                        }
+
+                        Animated.spring(animatedHeight, {
+                            toValue,
+                            useNativeDriver: false,
+                            friction: 10,
+                            tension: 40
+                        }).start();
+                        Animated.spring(animatedTranslateY, {
+                            toValue: 0,
+                            useNativeDriver: false,
+                            friction: 10,
+                            tension: 40
+                        }).start();
+                    } else {
+                        if (!isSwipeCloseRef.current) {
+                            Animated.spring(animatedHeight, {
+                                toValue: pivot,
+                                useNativeDriver: false,
+                                friction: 10,
+                                tension: 40
+                            }).start();
+                            Animated.timing(animatedTranslateY, {
+                                toValue: 0,
+                                useNativeDriver: false,
+                                duration: 300
+                            }).start();
+                        } else {
+                            let toValueT = 0;
+                            let isClosing = false;
+
+                            if (isFastSwipeDown) {
+                                toValueT = pivot + 100;
+                                isClosing = true;
+                            } else {
+                                if (currentT > pivot * 0.5) {
+                                    toValueT = pivot + 100;
+                                    isClosing = true;
+                                } else {
+                                    toValueT = 0;
+                                    isClosing = false;
+                                }
+                            }
+
+                            if (onClose) onClose({
+                                id: id.current
+                            });
+
+                            Animated.timing(animatedTranslateY, {
+                                toValue: toValueT,
+                                useNativeDriver: false,
+                                duration: 300
+                            }).start(({
+                                finished
+                            }) => {
+                                if (finished && isClosing) {
+                                    setIsActive(false);
+                                    if (onClosed) onClosed({
+                                        id: id.current
+                                    });
+                                }
+                            });
+                        }
+                    }
+                };
+
+                safeGlobal.addEventListener("pointermove", handlePointerMove);
+                safeGlobal.addEventListener("pointerup", handlePointerUp);
+            } : undefined}
             onLayout={onLayout}
             style={[
                 {
@@ -764,7 +1038,12 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
                         translateY: animatedTranslateY
                     }]
                 },
-                stylesheet.container
+                stylesheet.container,
+                webStyle({
+                    touchAction: "none",
+                    userSelect: "none",
+                    cursor: "grab"
+                })
             ]}
         >
             {renderHeader()}
@@ -786,7 +1065,12 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
                 ref={scrollViewRef}
                 bounces={false}
                 style={[
-                    scrollViewStyle
+                    scrollViewStyle,
+                    webStyle({
+                        maxWidth: windowWidth > 500 ? 500 : undefined,
+                        width: windowWidth > 500 ? 500 : undefined,
+                        alignSelf: "center"
+                    })
                 ]}
             >
                 {children}
@@ -801,6 +1085,20 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
             return null;
         }
 
+        if(Platform.OS === "web") {
+            return <View
+                style={[
+                    webStyle({
+                        maxWidth: windowWidth > 500 ? 500 : undefined,
+                        width: windowWidth > 500 ? 500 : undefined,
+                        alignSelf: "center"
+                    })
+                ]}
+            >
+                {RenderHeaderComponent()}
+            </View>;
+        }
+
         return RenderHeaderComponent();
     };
 
@@ -809,6 +1107,20 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
             return null;
         }
 
+        if(Platform.OS === "web") {
+            return <View
+                style={[
+                    webStyle({
+                        maxWidth: windowWidth > 500 ? 500 : undefined,
+                        width: windowWidth > 500 ? 500 : undefined,
+                        alignSelf: "center"
+                    })
+                ]}
+            >
+                <RenderBottomComponent/>
+            </View>;
+        }
+
         return <RenderBottomComponent/>;
     };
 
@@ -822,9 +1134,8 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
         const handleBorderRadius = Math.ceil(handleHeight / 2);
 
         return <View
-            {...panResponder.panHandlers}
-            onStartShouldSetResponderCapture={() => false}
-            onMoveShouldSetResponderCapture={() => false}
+            onStartShouldSetResponderCapture={() => Platform.OS === "web"}
+            onMoveShouldSetResponderCapture={() => Platform.OS === "web"}
             style={[
                 stylesheet.handleContainer,
                 {
@@ -833,9 +1144,14 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
                         : "transparent",
                     height: handleContainerHeight,
                     transform: [{
-                        translateY: -handleContainerHeight
+                        translateY: -handleContainerHeight * (Platform.OS === "web" ? 1.75 : 1)
                     }]
-                }
+                },
+                webStyle({
+                    touchAction: "none !important",
+                    userSelect: "none",
+                    cursor: "grab"
+                })
             ]}
         >
             <View

+ 9 - 0
src/components/checkBox/index.tsx

@@ -3,6 +3,7 @@ import {
 } from "react";
 import {
     TouchableOpacity,
+    Platform,
     View
 } from "react-native";
 import type ICheckBoxProps from "./type";
@@ -253,6 +254,14 @@ const CheckBox: FC<ICheckBoxProps> = ({
                 stylesheet.container,
                 containerDynamicStyle
             ]}
+            {...Platform.select({
+                web: {
+                    dataSet: {
+                        disablePressableDownEffect: "true"
+                    }
+                },
+                default: {}
+            })}
         >
             {isFlip ? null : renderCheckIndicator()}
 

+ 10 - 0
src/components/modal/index.tsx

@@ -8,6 +8,7 @@ import {
 import {
     TouchableWithoutFeedback,
     Animated,
+    Platform,
     Easing,
     View
 } from "react-native";
@@ -167,6 +168,15 @@ const Modal: RefForwardingComponent<IModalRef, IModalProps> = ({
                         backgroundColor: colors.system.scrim
                     }
                 ]}
+                {...Platform.select({
+                    web: {
+                        dataSet: {
+                            disablePressableHoverEffect: "true",
+                            disablePressableDownEffect: "true"
+                        }
+                    },
+                    default: {}
+                })}
             />
         </TouchableWithoutFeedback>;
     };

+ 7 - 3
src/components/monthSelector/index.tsx

@@ -28,8 +28,9 @@ import {
     NCoreUIKitTheme,
     NCoreUIKitToast
 } from "../../core/hooks";
-import type {
-    Mutable
+import {
+    type Mutable,
+    webStyle
 } from "../../types";
 import {
     ChevronRight as ChevronRightIcon,
@@ -426,7 +427,10 @@ const MonthSelector = ({
                 return <View
                     key={`month-row-${mIndex}`}
                     style={[
-                        stylesheet.rowContainer
+                        stylesheet.rowContainer,
+                        webStyle({
+                            width: "100%"
+                        })
                     ]}
                 >
                     {currentMonths.map((monthItem, monthIndex) => {

+ 9 - 0
src/components/numericInput/index.tsx

@@ -8,6 +8,7 @@ import {
     TextInput as NativeTextInput,
     TouchableOpacity,
     Keyboard,
+    Platform,
     View
 } from "react-native";
 import type ITextInputProps from "./type";
@@ -581,6 +582,14 @@ const NumericInput: RefForwardingComponent<INumericInputRef, ITextInputProps> =
         onPress={() => {
             if(!isDisabled) inputRef.current?.focus();
         }}
+        {...Platform.select({
+            web: {
+                dataSet: {
+                    disablePressableDownEffect: "true"
+                }
+            },
+            default: {}
+        })}
     >
         {renderTitle()}
 

+ 9 - 0
src/components/radioButton/index.tsx

@@ -3,6 +3,7 @@ import {
 } from "react";
 import {
     TouchableOpacity,
+    Platform,
     View
 } from "react-native";
 import type IRadioButtonProps from "./type";
@@ -248,6 +249,14 @@ const RadioButton: FC<IRadioButtonProps> = ({
                 stylesheet.container,
                 containerDynamicStyle
             ]}
+            {...Platform.select({
+                web: {
+                    dataSet: {
+                        disablePressableDownEffect: "true"
+                    }
+                },
+                default: {}
+            })}
         >
             {isFlip ? null : renderIndicatorContainer()}
 

+ 63 - 3
src/components/snackBar/index.tsx

@@ -1,4 +1,5 @@
 import {
+    type PointerEvent,
     useEffect,
     useState,
     type FC,
@@ -7,6 +8,7 @@ import {
 import {
     TouchableWithoutFeedback,
     PanResponder,
+    Platform,
     Animated,
     Easing,
     View
@@ -22,17 +24,21 @@ import {
 import {
     useSafeAreaInsets
 } from "react-native-safe-area-context";
+import {
+    safeGlobal,
+    type SafePointerEvent
+} from "../../types";
 import {
     X as XIcon
 } from "lucide-react-native";
 import {
     Portal
 } from "../../helpers/portalize";
-import Button from "../button";
-import Text from "../text";
 import {
     windowHeight
 } from "../../utils";
+import Button from "../button";
+import Text from "../text";
 
 const SnackBar: FC<ISnackBarProps> = ({
     isCloseOnPressActionButton = true,
@@ -331,7 +337,7 @@ const SnackBar: FC<ISnackBarProps> = ({
 
     return <Portal name="snack-bar-system">
         <Animated.View
-            {...panResponder.panHandlers}
+            {...(Platform.OS === "web" ? {} : panResponder.panHandlers)}
             onLayout={(event) => {
                 const _contentHeight = event.nativeEvent.layout.height;
 
@@ -339,6 +345,60 @@ const SnackBar: FC<ISnackBarProps> = ({
 
                 setIsMeasured(true);
             }}
+            onPointerDown={Platform.OS === "web" ? (evt) => {
+                const nativeEvt = evt as unknown as PointerEvent;
+                const startY = nativeEvt.clientY;
+
+                let lastY = startY;
+                let lastTime = Date.now();
+                let velocityY = 0;
+
+                const handlePointerMove = (moveEvt: SafePointerEvent) => {
+                    const dy = moveEvt.clientY - startY;
+
+                    if (dy > 0) return; // sadece yukarı swipe
+
+                    const now = Date.now();
+                    const dt = now - lastTime;
+                    if (dt > 0) velocityY = (moveEvt.clientY - lastY) / dt;
+                    lastY = moveEvt.clientY;
+                    lastTime = now;
+
+                    const op = Math.max(0, 1 + (dy / contentHeight.current));
+                    opacityAnim.setValue(op);
+                    transformAnim.setValue(dy);
+                };
+
+                const handlePointerUp = () => {
+                    safeGlobal.removeEventListener("pointermove", handlePointerMove);
+                    safeGlobal.removeEventListener("pointerup", handlePointerUp);
+
+                    const dy = lastY - startY;
+
+                    if (velocityY < -0.5 || dy < -25) {
+                        closeAnimation();
+                        return;
+                    }
+
+                    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();
+                };
+
+                safeGlobal.addEventListener("pointermove", handlePointerMove);
+                safeGlobal.addEventListener("pointerup", handlePointerUp);
+            } : undefined}
             style={[
                 style,
                 stylesheet.container,

+ 9 - 0
src/components/switch/index.tsx

@@ -6,6 +6,7 @@ import {
 import {
     TouchableOpacity,
     Animated,
+    Platform,
     Easing,
     View
 } from "react-native";
@@ -263,6 +264,14 @@ const Switch: FC<ISwitchProps> = ({
                 stylesheet.container,
                 containerDynamicStyle
             ]}
+            {...Platform.select({
+                web: {
+                    dataSet: {
+                        disablePressableDownEffect: "true"
+                    }
+                },
+                default: {}
+            })}
         >
             {isFlip ? null : renderIndicatorContainer()}
 

+ 9 - 0
src/components/textAreaInput/index.tsx

@@ -9,6 +9,7 @@ import {
     TextInput as NativeTextInput,
     TouchableOpacity,
     Keyboard,
+    Platform,
     View
 } from "react-native";
 import type ITextAreaInputProps from "./type";
@@ -516,6 +517,14 @@ const TextAreaInput: RefForwardingComponent<ITextAreaInputRef, ITextAreaInputPro
         onPress={() => {
             if(!isDisabled) inputRef.current?.focus();
         }}
+        {...Platform.select({
+            web: {
+                dataSet: {
+                    disablePressableDownEffect: "true"
+                }
+            },
+            default: {}
+        })}
     >
         {renderTitle()}
 

+ 9 - 0
src/components/textInput/index.tsx

@@ -8,6 +8,7 @@ import {
     TextInput as NativeTextInput,
     TouchableOpacity,
     Keyboard,
+    Platform,
     View
 } from "react-native";
 import type ITextInputProps from "./type";
@@ -519,6 +520,14 @@ const TextInput: RefForwardingComponent<ITextInputRef, ITextInputProps> = ({
         onPress={() => {
             if(!isDisabled) inputRef.current?.focus();
         }}
+        {...Platform.select({
+            web: {
+                dataSet: {
+                    disablePressableDownEffect: "true"
+                }
+            },
+            default: {}
+        })}
     >
         {renderTitle()}
 

+ 7 - 3
src/components/yearSelector/index.tsx

@@ -28,8 +28,9 @@ import {
     NCoreUIKitTheme,
     NCoreUIKitToast
 } from "../../core/hooks";
-import type {
-    Mutable
+import {
+    type Mutable,
+    webStyle
 } from "../../types";
 import {
     ChevronRight as ChevronRightIcon,
@@ -423,7 +424,10 @@ const YearSelector = ({
                 return <View
                     key={`year-row-${yIndex}`}
                     style={[
-                        stylesheet.rowContainer
+                        stylesheet.rowContainer,
+                        webStyle({
+                            width: "100%"
+                        })
                     ]}
                 >
                     {currentYears.map((yearItem, yearIndex) => {

+ 75 - 1
src/core/index.tsx

@@ -1,6 +1,9 @@
 import {
     type ReactNode
 } from "react";
+import {
+    Platform
+} from "react-native";
 import {
     initializeInstances
 } from "./hooks";
@@ -10,8 +13,8 @@ import type {
 import {
     SafeAreaProvider
 } from "react-native-safe-area-context";
-import CoreContext from "../context";
 import packageJSON from "../../package.json";
+import CoreContext from "../context";
 
 export const getNCoreUIKitVersion = () => packageJSON.version;
 
@@ -47,6 +50,77 @@ export const setupNCoreUIKit = (initialState: NCoreUIKitConfig, isCustom: boolea
 
         initializeInstances(NCoreUIKit);
 
+        if (Platform.OS === "web") {
+            const globalRef = globalThis as unknown as { document?: unknown };
+
+            if (globalRef && typeof globalRef.document === "object" && globalRef.document !== null) {
+                const doc = globalRef.document as {
+                    getElementById?: (id: string) => unknown;
+                    createElement?: (tagName: string) => unknown;
+                    head?: unknown;
+                };
+
+                const styleId = "cl-rnw-touchable-fix";
+
+                if (typeof doc.getElementById === "function" && !doc.getElementById(styleId)) {
+                    if (typeof doc.createElement === "function" && doc.head && typeof doc.head === "object") {
+
+                        const style = doc.createElement("style") as {
+                            id?: string;
+                            type?: string;
+                            innerHTML?: string;
+                        };
+
+                        if (style) {
+                            style.id = styleId;
+                            style.type = "text/css";
+                            style.innerHTML = `
+                                html, body, #root {
+                                    touch-action: none;
+                                    overflow: hidden;
+                                }
+
+                                [role="button"],
+                                [data-focusable="true"],
+                                div[tabindex="0"] {
+                                    cursor: pointer !important;
+                                    transition: opacity 0.15s ease-in-out, transform 0.1s ease-in-out !important;
+                                }
+
+                                [role="button"]:not([data-disable-pressable-hover-effect="true"]):hover,
+                                [data-focusable="true"]:not([data-disable-pressable-hover-effect="true"]):hover,
+                                div[tabindex="0"]:not([data-disable-pressable-hover-effect="true"]):hover {
+                                    opacity: 0.8 !important;
+                                }
+
+                                [role="button"]:not([data-disable-pressable-down-effect="true"]):active,
+                                [data-focusable="true"]:not([data-disable-pressable-down-effect="true"]):active,
+                                div[tabindex="0"]:not([data-disable-pressable-down-effect="true"]):active {
+                                    opacity: 0.5 !important;
+                                    transform: translateY(2px) !important;
+                                }
+
+                                input,
+                                textarea,
+                                select,
+                                [role="textinput"],
+                                div[tabindex]:focus {
+                                    outline: none !important;
+                                    box-shadow: none !important;
+                                }
+                            `;
+
+                            const head = doc.head as { appendChild?: (node: unknown) => void };
+
+                            if (typeof head.appendChild === "function") {
+                                head.appendChild(style);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
         return NCoreUIKit;
     }
 };

+ 18 - 0
src/types/index.ts

@@ -1,6 +1,11 @@
 import {
     type ForwardRefRenderFunction
 } from "react";
+import {
+    type StyleProp,
+    type ViewStyle,
+    Platform
+} from "react-native";
 import {
     type INCoreUIKitIconCallbackProps
 } from "./icon";
@@ -55,12 +60,25 @@ export type {
     ThemeType
 };
 
+export const webStyle = (styles: Record<string, unknown>): StyleProp<ViewStyle> => Platform.OS === "web" ? (styles as StyleProp<ViewStyle>) : {};
+
 export type RecursiveRecord = {
   [key: string]: string | RecursiveRecord;
 };
 
 export interface RefForwardingComponent<T, P = object> extends ForwardRefRenderFunction<T, P> {};
 
+export interface SafePointerEvent {
+    clientY: number;
+}
+
+export interface SafeWebWindow {
+    addEventListener: (type: "pointermove" | "pointerup", listener: (evt: SafePointerEvent) => void) => void;
+    removeEventListener: (type: "pointermove" | "pointerup", listener: (evt: SafePointerEvent) => void) => void;
+}
+
+export const safeGlobal = globalThis as unknown as SafeWebWindow;
+
 export type Mutable<T> = {
     -readonly [P in keyof T]: T[P];
 };