related-ui-components 4.0.3 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ import { Portal, PortalHost, PortalProvider } from "@gorhom/portal";
4
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
5
+ import { BackHandler, Platform, StyleSheet, TouchableOpacity, View } from "react-native";
6
+ import { KeyboardAvoidingView } from "react-native-keyboard-controller";
7
+ import Animated, { Easing, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
8
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
9
+
10
+ // --- Types ---
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ // --- Context ---
13
+
14
+ const ModalContext = /*#__PURE__*/createContext(undefined);
15
+ export const DEFAULT_MODAL_HOST = "UniversalModalHost";
16
+
17
+ // --- Default Modal UI Component ---
18
+ const DefaultModalUI = ({
19
+ isVisible,
20
+ children,
21
+ options,
22
+ onClose
23
+ }) => {
24
+ const insets = useSafeAreaInsets();
25
+ const [isRendered, setIsRendered] = useState(isVisible);
26
+ const opacity = useSharedValue(0);
27
+ const {
28
+ closeOnBackdropPress = true,
29
+ closeOnBackButton = true,
30
+ animationConfig = {
31
+ duration: 300,
32
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1)
33
+ },
34
+ position = "center",
35
+ avoidKeyboard = true,
36
+ backdropStyle,
37
+ containerStyle
38
+ } = options;
39
+ useEffect(() => {
40
+ if (isVisible) setIsRendered(true);
41
+ opacity.value = withTiming(isVisible ? 1 : 0, animationConfig, finished => {
42
+ if (finished && !isVisible) {
43
+ runOnJS(setIsRendered)(false);
44
+ }
45
+ });
46
+ }, [isVisible, animationConfig]);
47
+ useEffect(() => {
48
+ const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
49
+ if (isVisible && closeOnBackButton) {
50
+ onClose();
51
+ return true;
52
+ }
53
+ return false;
54
+ });
55
+ return () => backHandler.remove();
56
+ }, [isVisible, closeOnBackButton, onClose]);
57
+ const backdropAnimatedStyle = useAnimatedStyle(() => ({
58
+ opacity: opacity.value
59
+ }));
60
+ const contentAnimatedStyle = useAnimatedStyle(() => {
61
+ // Simple slide/fade effect based on position
62
+ let transform = [];
63
+ if (position === "bottom") {
64
+ transform.push({
65
+ translateY: (1 - opacity.value) * 100
66
+ });
67
+ } else if (position === "top") {
68
+ transform.push({
69
+ translateY: (1 - opacity.value) * -100
70
+ });
71
+ } else {
72
+ transform.push({
73
+ scale: 0.9 + opacity.value * 0.1
74
+ });
75
+ }
76
+ return {
77
+ opacity: opacity.value,
78
+ transform
79
+ };
80
+ });
81
+ if (!isRendered) return null;
82
+ const justifyContent = position === "bottom" ? "flex-end" : position === "top" ? "flex-start" : "center";
83
+ const paddingBottom = position === "bottom" ? insets.bottom : 0;
84
+ const paddingTop = position === "top" ? insets.top : 0;
85
+ const ContentWrapper = avoidKeyboard ? KeyboardAvoidingView : View;
86
+ return /*#__PURE__*/_jsxs(View, {
87
+ style: [StyleSheet.absoluteFill, styles.overlayWrapper, {
88
+ justifyContent
89
+ }],
90
+ children: [/*#__PURE__*/_jsx(Animated.View, {
91
+ style: [StyleSheet.absoluteFill, styles.backdrop, backdropStyle, backdropAnimatedStyle],
92
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
93
+ style: StyleSheet.absoluteFill,
94
+ activeOpacity: 1,
95
+ onPress: closeOnBackdropPress ? onClose : undefined
96
+ })
97
+ }), /*#__PURE__*/_jsx(ContentWrapper, {
98
+ behavior: Platform.OS === "ios" ? "padding" : undefined,
99
+ style: [styles.keyboardView, {
100
+ paddingBottom,
101
+ paddingTop
102
+ }],
103
+ pointerEvents: "box-none",
104
+ children: /*#__PURE__*/_jsx(Animated.View, {
105
+ style: [styles.baseContainer, containerStyle, contentAnimatedStyle],
106
+ children: children
107
+ })
108
+ })]
109
+ });
110
+ };
111
+
112
+ // --- Provider ---
113
+
114
+ export const UniversalModalProvider = ({
115
+ children,
116
+ defaultOptions = {},
117
+ portalHostName = DEFAULT_MODAL_HOST,
118
+ CustomModalComponent
119
+ }) => {
120
+ const [state, setState] = useState({
121
+ isVisible: false,
122
+ content: null,
123
+ options: defaultOptions
124
+ });
125
+ const showModal = useCallback((content, options) => {
126
+ setState({
127
+ isVisible: true,
128
+ content,
129
+ options: {
130
+ ...defaultOptions,
131
+ ...options
132
+ }
133
+ });
134
+ }, [defaultOptions]);
135
+ const hideModal = useCallback(() => {
136
+ setState(prev => ({
137
+ ...prev,
138
+ isVisible: false
139
+ }));
140
+ // Clear content after animation normally finishes to avoid flicker
141
+ setTimeout(() => {
142
+ setState(prev => {
143
+ // Only clear if still hidden (prevent race condition if immediately reshown)
144
+ return !prev.isVisible ? {
145
+ ...prev,
146
+ content: null
147
+ } : prev;
148
+ });
149
+ }, 350);
150
+ }, []);
151
+ const ModalUI = CustomModalComponent || DefaultModalUI;
152
+ const value = useMemo(() => ({
153
+ showModal,
154
+ hideModal,
155
+ isVisible: state.isVisible
156
+ }), [showModal, hideModal, state.isVisible]);
157
+ return /*#__PURE__*/_jsx(ModalContext.Provider, {
158
+ value: value,
159
+ children: /*#__PURE__*/_jsxs(PortalProvider, {
160
+ children: [children, /*#__PURE__*/_jsx(PortalHost, {
161
+ name: portalHostName
162
+ }), /*#__PURE__*/_jsx(Portal, {
163
+ hostName: portalHostName,
164
+ children: /*#__PURE__*/_jsx(ModalUI, {
165
+ isVisible: state.isVisible,
166
+ options: state.options,
167
+ onClose: hideModal,
168
+ children: state.content
169
+ })
170
+ })]
171
+ })
172
+ });
173
+ };
174
+
175
+ // --- Hook ---
176
+
177
+ export const useModal = () => {
178
+ const context = useContext(ModalContext);
179
+ if (!context) {
180
+ throw new Error("useModal must be used within a UniversalModalProvider");
181
+ }
182
+ return context;
183
+ };
184
+
185
+ // --- Default Styles ---
186
+
187
+ const styles = StyleSheet.create({
188
+ overlayWrapper: {
189
+ zIndex: 1000,
190
+ elevation: 1000
191
+ },
192
+ backdrop: {
193
+ backgroundColor: "rgba(0, 0, 0, 0.6)"
194
+ },
195
+ keyboardView: {
196
+ flexGrow: 1,
197
+ justifyContent: "center",
198
+ alignItems: "center"
199
+ },
200
+ baseContainer: {
201
+ backgroundColor: "white",
202
+ // Default, allows override
203
+ borderRadius: 16,
204
+ padding: 20,
205
+ width: "90%",
206
+ maxWidth: 500,
207
+ alignItems: "center",
208
+ shadowColor: "#000",
209
+ shadowOffset: {
210
+ width: 0,
211
+ height: 2
212
+ },
213
+ shadowOpacity: 0.25,
214
+ shadowRadius: 3.84,
215
+ elevation: 5
216
+ }
217
+ });
218
+ //# sourceMappingURL=UniversalModalProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["Portal","PortalHost","PortalProvider","React","createContext","useCallback","useContext","useEffect","useMemo","useState","BackHandler","Platform","StyleSheet","TouchableOpacity","View","KeyboardAvoidingView","Animated","Easing","runOnJS","useAnimatedStyle","useSharedValue","withTiming","useSafeAreaInsets","jsx","_jsx","jsxs","_jsxs","ModalContext","undefined","DEFAULT_MODAL_HOST","DefaultModalUI","isVisible","children","options","onClose","insets","isRendered","setIsRendered","opacity","closeOnBackdropPress","closeOnBackButton","animationConfig","duration","easing","bezier","position","avoidKeyboard","backdropStyle","containerStyle","value","finished","backHandler","addEventListener","remove","backdropAnimatedStyle","contentAnimatedStyle","transform","push","translateY","scale","justifyContent","paddingBottom","bottom","paddingTop","top","ContentWrapper","style","absoluteFill","styles","overlayWrapper","backdrop","activeOpacity","onPress","behavior","OS","keyboardView","pointerEvents","baseContainer","UniversalModalProvider","defaultOptions","portalHostName","CustomModalComponent","state","setState","content","showModal","hideModal","prev","setTimeout","ModalUI","Provider","name","hostName","useModal","context","Error","create","zIndex","elevation","backgroundColor","flexGrow","alignItems","borderRadius","padding","width","maxWidth","shadowColor","shadowOffset","height","shadowOpacity","shadowRadius"],"sourceRoot":"..\\..\\..\\src","sources":["contexts/UniversalModalProvider.tsx"],"mappings":"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","ignoreList":[]}
@@ -2,4 +2,5 @@
2
2
 
3
3
  export * from "./BottomSheetStackContext.js";
4
4
  export * from "./BottomSheetStackProvider.js";
5
+ export * from "./UniversalModalProvider.js";
5
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["contexts/index.ts"],"mappings":";;AAAA,cAAc,8BAA2B;AACzC,cAAc,+BAA4B","ignoreList":[]}
1
+ {"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["contexts/index.ts"],"mappings":";;AAAA,cAAc,8BAA2B;AACzC,cAAc,+BAA4B;AAC1C,cAAc,6BAA0B","ignoreList":[]}
@@ -0,0 +1,43 @@
1
+ import React, { ReactNode } from "react";
2
+ import { StyleProp, ViewStyle } from "react-native";
3
+ import { WithTimingConfig } from "react-native-reanimated";
4
+ export interface ModalOptions {
5
+ /** Should the modal close when tapping the backdrop? Default: true */
6
+ closeOnBackdropPress?: boolean;
7
+ /** Should the hardware back button close the modal? Default: true */
8
+ closeOnBackButton?: boolean;
9
+ /** Animation configuration for entry/exit */
10
+ animationConfig?: WithTimingConfig;
11
+ /** Custom styles for the backdrop */
12
+ backdropStyle?: StyleProp<ViewStyle>;
13
+ /** Custom styles for the modal container (the white box) */
14
+ containerStyle?: StyleProp<ViewStyle>;
15
+ /** Position of the modal. Default: 'center' */
16
+ position?: "center" | "bottom" | "top";
17
+ /** If true, avoids keyboard view. Default: true */
18
+ avoidKeyboard?: boolean;
19
+ }
20
+ interface ModalContextType {
21
+ showModal: (content: ReactNode, options?: ModalOptions) => void;
22
+ hideModal: () => void;
23
+ isVisible: boolean;
24
+ }
25
+ interface ModalProviderProps {
26
+ children: ReactNode;
27
+ /** Global default options for all modals */
28
+ defaultOptions?: ModalOptions;
29
+ /** Optional: Provide a specific Portal Host name if nesting providers */
30
+ portalHostName?: string;
31
+ /** Optional: Override the internal Modal UI component completely */
32
+ CustomModalComponent?: React.FC<{
33
+ isVisible: boolean;
34
+ children: ReactNode;
35
+ options: ModalOptions;
36
+ onClose: () => void;
37
+ }>;
38
+ }
39
+ export declare const DEFAULT_MODAL_HOST = "UniversalModalHost";
40
+ export declare const UniversalModalProvider: React.FC<ModalProviderProps>;
41
+ export declare const useModal: () => ModalContextType;
42
+ export {};
43
+ //# sourceMappingURL=UniversalModalProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UniversalModalProvider.d.ts","sourceRoot":"","sources":["../../../../src/contexts/UniversalModalProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAEZ,SAAS,EAMV,MAAM,OAAO,CAAC;AACf,OAAO,EAGL,SAAS,EAIT,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAiB,EAQf,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,6CAA6C;IAC7C,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,qCAAqC;IACrC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,4DAA4D;IAC5D,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;IACvC,mDAAmD;IACnD,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,UAAU,gBAAgB;IACxB,SAAS,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IAChE,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,SAAS,CAAC;IACpB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,YAAY,CAAC;IAC9B,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9B,SAAS,EAAE,OAAO,CAAC;QACnB,QAAQ,EAAE,SAAS,CAAC;QACpB,OAAO,EAAE,YAAY,CAAC;QACtB,OAAO,EAAE,MAAM,IAAI,CAAC;KACrB,CAAC,CAAC;CACJ;AAMD,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAgHvD,eAAO,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAiE/D,CAAC;AAIF,eAAO,MAAM,QAAQ,wBAMpB,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export * from "./BottomSheetStackContext";
2
2
  export * from "./BottomSheetStackProvider";
3
+ export * from "./UniversalModalProvider";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/contexts/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/contexts/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "related-ui-components",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "main": "./src/index.ts",
5
5
  "scripts": {
6
6
  "start": "expo start",
@@ -34,7 +34,8 @@
34
34
  "react-native-reanimated": ">=3.0.0",
35
35
  "react-native-safe-area-context": ">=5.4.0",
36
36
  "react-native-svg": ">=15.11.2",
37
- "react-native-toast-message": ">=2.3.3"
37
+ "react-native-toast-message": ">=2.3.3",
38
+ "react-native-keyboard-controller": "^1.18.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@babel/core": "^7.25.2",
@@ -0,0 +1,293 @@
1
+ import { Portal, PortalHost, PortalProvider } from "@gorhom/portal";
2
+ import React, {
3
+ createContext,
4
+ ReactNode,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
11
+ import {
12
+ BackHandler,
13
+ Platform,
14
+ StyleProp,
15
+ StyleSheet,
16
+ TouchableOpacity,
17
+ View,
18
+ ViewStyle,
19
+ } from "react-native";
20
+ import { KeyboardProvider as RNKeyboardProvider, KeyboardAvoidingView } from "react-native-keyboard-controller";
21
+ import Animated, {
22
+ Easing,
23
+ FadeIn,
24
+ FadeOut,
25
+ runOnJS,
26
+ useAnimatedStyle,
27
+ useSharedValue,
28
+ withTiming,
29
+ WithTimingConfig,
30
+ } from "react-native-reanimated";
31
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
32
+
33
+ // --- Types ---
34
+ export interface ModalOptions {
35
+ /** Should the modal close when tapping the backdrop? Default: true */
36
+ closeOnBackdropPress?: boolean;
37
+ /** Should the hardware back button close the modal? Default: true */
38
+ closeOnBackButton?: boolean;
39
+ /** Animation configuration for entry/exit */
40
+ animationConfig?: WithTimingConfig;
41
+ /** Custom styles for the backdrop */
42
+ backdropStyle?: StyleProp<ViewStyle>;
43
+ /** Custom styles for the modal container (the white box) */
44
+ containerStyle?: StyleProp<ViewStyle>;
45
+ /** Position of the modal. Default: 'center' */
46
+ position?: "center" | "bottom" | "top";
47
+ /** If true, avoids keyboard view. Default: true */
48
+ avoidKeyboard?: boolean;
49
+ }
50
+
51
+ interface ModalContextType {
52
+ showModal: (content: ReactNode, options?: ModalOptions) => void;
53
+ hideModal: () => void;
54
+ isVisible: boolean;
55
+ }
56
+
57
+ interface ModalProviderProps {
58
+ children: ReactNode;
59
+ /** Global default options for all modals */
60
+ defaultOptions?: ModalOptions;
61
+ /** Optional: Provide a specific Portal Host name if nesting providers */
62
+ portalHostName?: string;
63
+ /** Optional: Override the internal Modal UI component completely */
64
+ CustomModalComponent?: React.FC<{
65
+ isVisible: boolean;
66
+ children: ReactNode;
67
+ options: ModalOptions;
68
+ onClose: () => void;
69
+ }>;
70
+ }
71
+
72
+ // --- Context ---
73
+
74
+ const ModalContext = createContext<ModalContextType | undefined>(undefined);
75
+
76
+ export const DEFAULT_MODAL_HOST = "UniversalModalHost";
77
+
78
+ // --- Default Modal UI Component ---
79
+ const DefaultModalUI: React.FC<{
80
+ isVisible: boolean;
81
+ children: ReactNode;
82
+ options: ModalOptions;
83
+ onClose: () => void;
84
+ }> = ({ isVisible, children, options, onClose }) => {
85
+ const insets = useSafeAreaInsets();
86
+ const [isRendered, setIsRendered] = useState(isVisible);
87
+ const opacity = useSharedValue(0);
88
+
89
+ const {
90
+ closeOnBackdropPress = true,
91
+ closeOnBackButton = true,
92
+ animationConfig = { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) },
93
+ position = "center",
94
+ avoidKeyboard = true,
95
+ backdropStyle,
96
+ containerStyle,
97
+ } = options;
98
+
99
+ useEffect(() => {
100
+ if (isVisible) setIsRendered(true);
101
+
102
+ opacity.value = withTiming(
103
+ isVisible ? 1 : 0,
104
+ animationConfig,
105
+ (finished) => {
106
+ if (finished && !isVisible) {
107
+ runOnJS(setIsRendered)(false);
108
+ }
109
+ }
110
+ );
111
+ }, [isVisible, animationConfig]);
112
+
113
+ useEffect(() => {
114
+ const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
115
+ if (isVisible && closeOnBackButton) {
116
+ onClose();
117
+ return true;
118
+ }
119
+ return false;
120
+ });
121
+ return () => backHandler.remove();
122
+ }, [isVisible, closeOnBackButton, onClose]);
123
+
124
+ const backdropAnimatedStyle = useAnimatedStyle(() => ({
125
+ opacity: opacity.value,
126
+ }));
127
+
128
+ const contentAnimatedStyle = useAnimatedStyle(() => {
129
+ // Simple slide/fade effect based on position
130
+ let transform = [];
131
+ if (position === "bottom") {
132
+ transform.push({ translateY: (1 - opacity.value) * 100 });
133
+ } else if (position === "top") {
134
+ transform.push({ translateY: (1 - opacity.value) * -100 });
135
+ } else {
136
+ transform.push({ scale: 0.9 + opacity.value * 0.1 });
137
+ }
138
+
139
+ return {
140
+ opacity: opacity.value,
141
+ transform,
142
+ };
143
+ });
144
+
145
+ if (!isRendered) return null;
146
+
147
+ const justifyContent =
148
+ position === "bottom" ? "flex-end" : position === "top" ? "flex-start" : "center";
149
+
150
+ const paddingBottom = position === "bottom" ? insets.bottom : 0;
151
+ const paddingTop = position === "top" ? insets.top : 0;
152
+
153
+ const ContentWrapper = avoidKeyboard ? KeyboardAvoidingView : View;
154
+
155
+ return (
156
+ <View style={[StyleSheet.absoluteFill, styles.overlayWrapper, { justifyContent }]}>
157
+ {/* Backdrop */}
158
+ <Animated.View style={[StyleSheet.absoluteFill, styles.backdrop, backdropStyle, backdropAnimatedStyle]}>
159
+ <TouchableOpacity
160
+ style={StyleSheet.absoluteFill}
161
+ activeOpacity={1}
162
+ onPress={closeOnBackdropPress ? onClose : undefined}
163
+ />
164
+ </Animated.View>
165
+
166
+ {/* Content */}
167
+ <ContentWrapper
168
+ behavior={Platform.OS === "ios" ? "padding" : undefined}
169
+ style={[styles.keyboardView, { paddingBottom, paddingTop }]}
170
+ pointerEvents="box-none"
171
+ >
172
+ <Animated.View
173
+ style={[
174
+ styles.baseContainer,
175
+ containerStyle,
176
+ contentAnimatedStyle,
177
+ ]}
178
+ >
179
+ {children}
180
+ </Animated.View>
181
+ </ContentWrapper>
182
+ </View>
183
+ );
184
+ };
185
+
186
+ // --- Provider ---
187
+
188
+ export const UniversalModalProvider: React.FC<ModalProviderProps> = ({
189
+ children,
190
+ defaultOptions = {},
191
+ portalHostName = DEFAULT_MODAL_HOST,
192
+ CustomModalComponent,
193
+ }) => {
194
+ const [state, setState] = useState<{
195
+ isVisible: boolean;
196
+ content: ReactNode | null;
197
+ options: ModalOptions;
198
+ }>({
199
+ isVisible: false,
200
+ content: null,
201
+ options: defaultOptions,
202
+ });
203
+
204
+ const showModal = useCallback(
205
+ (content: ReactNode, options?: ModalOptions) => {
206
+ setState({
207
+ isVisible: true,
208
+ content,
209
+ options: { ...defaultOptions, ...options },
210
+ });
211
+ },
212
+ [defaultOptions]
213
+ );
214
+
215
+ const hideModal = useCallback(() => {
216
+ setState((prev) => ({ ...prev, isVisible: false }));
217
+ // Clear content after animation normally finishes to avoid flicker
218
+ setTimeout(() => {
219
+ setState((prev) => {
220
+ // Only clear if still hidden (prevent race condition if immediately reshown)
221
+ return !prev.isVisible ? { ...prev, content: null } : prev
222
+ });
223
+ }, 350);
224
+ }, []);
225
+
226
+ const ModalUI = CustomModalComponent || DefaultModalUI;
227
+
228
+ const value = useMemo(
229
+ () => ({ showModal, hideModal, isVisible: state.isVisible }),
230
+ [showModal, hideModal, state.isVisible]
231
+ );
232
+
233
+ return (
234
+ <ModalContext.Provider value={value}>
235
+ {/* Ensure PortalProvider exists if not wrapped at root */}
236
+ {/* Note: If you already have PortalProvider at root, this nested one might not be strictly necessary depending on usage,
237
+ but wrapping the host ensures the modal renders here. */}
238
+ <PortalProvider>
239
+ {children}
240
+ <PortalHost name={portalHostName} />
241
+ <Portal hostName={portalHostName}>
242
+ <ModalUI
243
+ isVisible={state.isVisible}
244
+ options={state.options}
245
+ onClose={hideModal}
246
+ >
247
+ {state.content}
248
+ </ModalUI>
249
+ </Portal>
250
+ </PortalProvider>
251
+ </ModalContext.Provider>
252
+ );
253
+ };
254
+
255
+ // --- Hook ---
256
+
257
+ export const useModal = () => {
258
+ const context = useContext(ModalContext);
259
+ if (!context) {
260
+ throw new Error("useModal must be used within a UniversalModalProvider");
261
+ }
262
+ return context;
263
+ };
264
+
265
+ // --- Default Styles ---
266
+
267
+ const styles = StyleSheet.create({
268
+ overlayWrapper: {
269
+ zIndex: 1000,
270
+ elevation: 1000,
271
+ },
272
+ backdrop: {
273
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
274
+ },
275
+ keyboardView: {
276
+ flexGrow: 1,
277
+ justifyContent: "center",
278
+ alignItems: "center",
279
+ },
280
+ baseContainer: {
281
+ backgroundColor: "white", // Default, allows override
282
+ borderRadius: 16,
283
+ padding: 20,
284
+ width: "90%",
285
+ maxWidth: 500,
286
+ alignItems: "center",
287
+ shadowColor: "#000",
288
+ shadowOffset: { width: 0, height: 2 },
289
+ shadowOpacity: 0.25,
290
+ shadowRadius: 3.84,
291
+ elevation: 5,
292
+ },
293
+ });
@@ -1,2 +1,3 @@
1
1
  export * from "./BottomSheetStackContext"
2
- export * from "./BottomSheetStackProvider"
2
+ export * from "./BottomSheetStackProvider"
3
+ export * from "./UniversalModalProvider";