expo-app-ui 1.0.3 → 1.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.
package/README.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs.
4
4
 
5
+ ## Component Showcase
6
+
7
+ <div align="center">
8
+
9
+ <img src="https://expo-apps-ui.vercel.app/examples/buttons-example.png" alt="Button Component" width="150" />
10
+ <img src="https://expo-apps-ui.vercel.app/examples/custom-modal-example.gif" alt="Custom Modal" width="150" />
11
+ <img src="https://expo-apps-ui.vercel.app/examples/otp-input-example.gif" alt="OTP Input" width="150" />
12
+ <img src="https://expo-apps-ui.vercel.app/examples/top-loading-bar-example.gif" alt="Top Loading Bar" width="150" />
13
+
14
+ *Button • Custom Modal • OTP Input • Loading Bar*
15
+
16
+ </div>
17
+
5
18
  ## 📚 Documentation
6
19
 
7
20
  **👉 [View Full Documentation →](https://expo-apps-ui.vercel.app)**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-ui",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs. Documentation: https://expo-apps-ui.vercel.app",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -6,8 +6,11 @@ import {
6
6
  StyleSheet,
7
7
  TouchableWithoutFeedback,
8
8
  useWindowDimensions,
9
- StyleProp, // Import StyleProp
10
- ViewStyle, // Import ViewStyle
9
+ StyleProp,
10
+ ViewStyle,
11
+ Keyboard,
12
+ Platform,
13
+ BackHandler,
11
14
  } from "react-native";
12
15
  import Animated, {
13
16
  useSharedValue,
@@ -17,14 +20,25 @@ import Animated, {
17
20
  runOnJS,
18
21
  } from "react-native-reanimated";
19
22
 
23
+ // Default colors - using black and white as defaults
24
+ const defaultColors = {
25
+ white: "#FFFFFF",
26
+ black: "#000000",
27
+ backdrop: "rgba(0, 0, 0, 0.5)",
28
+ };
29
+
20
30
  // Define the props interface
21
31
  interface CustomModalProps {
22
32
  visible: boolean;
23
33
  onClose: () => void;
24
34
  preventBackgroundTouchEvent?: boolean;
25
35
  children: React.ReactNode;
26
- style?: StyleProp<ViewStyle>; // <-- Prop for root container
27
- modalStyle?: StyleProp<ViewStyle>; // <-- Prop for the content box
36
+ style?: StyleProp<ViewStyle>; // Prop for root container
37
+ modalStyle?: StyleProp<ViewStyle>; // Prop for the content box
38
+ noBackdrop?: boolean; // Hide backdrop
39
+ backgroundColor?: string; // Modal background color
40
+ backdropColor?: string; // Backdrop color
41
+ borderRadius?: number; // Border radius for modal
28
42
  }
29
43
 
30
44
  const MODAL_ANIMATION_DURATION = 300;
@@ -36,27 +50,106 @@ const CustomModal: React.FC<CustomModalProps> = ({
36
50
  onClose,
37
51
  preventBackgroundTouchEvent,
38
52
  children,
39
- style, // <-- Destructure new prop
40
- modalStyle, // <-- Destructure new prop
53
+ style,
54
+ modalStyle,
55
+ noBackdrop = false,
56
+ backgroundColor = defaultColors.white,
57
+ backdropColor = defaultColors.backdrop,
58
+ borderRadius = 15,
41
59
  }) => {
42
60
  const { height } = useWindowDimensions();
43
61
 
44
62
  const [isModalRendered, setIsModalRendered] = useState(visible);
45
63
  const backdropOpacity = useSharedValue(0);
46
64
  const modalTranslateY = useSharedValue(height);
65
+ const keyboardOffset = useSharedValue(0);
66
+
67
+ // Check if modal is positioned at bottom (bottom sheet style)
68
+ const flattenedStyle = style ? StyleSheet.flatten(style) : {};
69
+ const isBottomSheet = (flattenedStyle as any)?.justifyContent === "flex-end";
47
70
 
48
71
  const backdropAnimatedStyle = useAnimatedStyle(() => ({
49
- opacity: backdropOpacity.value,
72
+ opacity: noBackdrop ? 0 : backdropOpacity.value,
50
73
  }));
51
74
 
52
- const modalAnimatedStyle = useAnimatedStyle(() => ({
53
- transform: [{ translateY: modalTranslateY.value }],
54
- }));
75
+ const modalAnimatedStyle = useAnimatedStyle(() => {
76
+ if (isBottomSheet) {
77
+ // For bottom sheets, position at bottom and translate from below
78
+ // Subtract keyboardOffset to move modal up when keyboard appears
79
+ return {
80
+ transform: [
81
+ {
82
+ translateY: modalTranslateY.value - keyboardOffset.value,
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ // For centered modals, use standard transform
88
+ return {
89
+ transform: [{ translateY: modalTranslateY.value - keyboardOffset.value }],
90
+ };
91
+ });
92
+
93
+ // Handle Android back button
94
+ useEffect(() => {
95
+ if (!visible) return;
96
+
97
+ const backHandler = BackHandler.addEventListener(
98
+ "hardwareBackPress",
99
+ () => {
100
+ if (!preventBackgroundTouchEvent) {
101
+ onClose();
102
+ return true; // Prevent default back behavior
103
+ }
104
+ return false; // Allow default back behavior if preventBackgroundTouchEvent is true
105
+ }
106
+ );
107
+
108
+ return () => backHandler.remove();
109
+ }, [visible, preventBackgroundTouchEvent, onClose]);
110
+
111
+ // Handle keyboard events for bottom sheet modals
112
+ useEffect(() => {
113
+ if (!visible || !isBottomSheet) {
114
+ // Reset keyboard offset when modal is not visible or not a bottom sheet
115
+ keyboardOffset.value = 0;
116
+ return;
117
+ }
118
+
119
+ const showEvent =
120
+ Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
121
+ const hideEvent =
122
+ Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
123
+
124
+ const keyboardWillShowListener = Keyboard.addListener(showEvent, (e) => {
125
+ const keyboardHeight = e.endCoordinates.height;
126
+ keyboardOffset.value = withTiming(keyboardHeight, {
127
+ duration: Platform.OS === "ios" ? e.duration || 250 : 250,
128
+ easing: Easing.out(Easing.ease),
129
+ });
130
+ });
131
+
132
+ const keyboardWillHideListener = Keyboard.addListener(hideEvent, () => {
133
+ keyboardOffset.value = withTiming(0, {
134
+ duration: 250,
135
+ easing: Easing.out(Easing.ease),
136
+ });
137
+ });
138
+
139
+ return () => {
140
+ keyboardWillShowListener.remove();
141
+ keyboardWillHideListener.remove();
142
+ };
143
+ }, [visible, isBottomSheet, keyboardOffset]);
55
144
 
56
145
  useEffect(() => {
57
146
  if (visible) {
58
147
  setIsModalRendered(true);
59
- backdropOpacity.value = withTiming(0.5, {
148
+ // Ensure modalTranslateY starts from height for bottom sheets
149
+ if (isBottomSheet) {
150
+ modalTranslateY.value = height;
151
+ }
152
+ backdropOpacity.value = withTiming(noBackdrop ? 0 : 0.5, {
60
153
  duration: MODAL_ANIMATION_DURATION,
61
154
  easing: backdropEasing,
62
155
  });
@@ -69,6 +162,10 @@ const CustomModal: React.FC<CustomModalProps> = ({
69
162
  duration: MODAL_ANIMATION_DURATION,
70
163
  easing: backdropEasing,
71
164
  });
165
+ keyboardOffset.value = withTiming(0, {
166
+ duration: MODAL_ANIMATION_DURATION,
167
+ easing: modalEasing,
168
+ });
72
169
  modalTranslateY.value = withTiming(
73
170
  height,
74
171
  {
@@ -82,23 +179,46 @@ const CustomModal: React.FC<CustomModalProps> = ({
82
179
  }
83
180
  );
84
181
  }
85
- }, [visible, height, backdropOpacity, modalTranslateY]);
182
+ }, [visible, height, isBottomSheet, backdropOpacity, modalTranslateY, keyboardOffset, noBackdrop]);
86
183
 
87
184
  if (!isModalRendered) {
88
185
  return null;
89
186
  }
90
187
 
91
188
  return (
92
- // Apply the custom root 'style' prop here
93
189
  <Animated.View style={[styles.container, style]}>
94
- {!preventBackgroundTouchEvent && (
95
- <TouchableWithoutFeedback onPress={onClose}>
96
- <Animated.View style={[styles.backdrop, backdropAnimatedStyle]} />
190
+ {!noBackdrop && (
191
+ <TouchableWithoutFeedback
192
+ onPress={() => {
193
+ // By default, backdrop touch closes the modal
194
+ // Only prevent if explicitly set to true
195
+ if (!preventBackgroundTouchEvent) {
196
+ onClose();
197
+ }
198
+ }}
199
+ >
200
+ <Animated.View
201
+ style={[
202
+ styles.backdrop,
203
+ { backgroundColor: backdropColor },
204
+ backdropAnimatedStyle,
205
+ ]}
206
+ />
97
207
  </TouchableWithoutFeedback>
98
208
  )}
99
209
 
100
- {/* Apply the custom 'modalStyle' prop here */}
101
- <Animated.View style={[styles.modalView, modalAnimatedStyle, modalStyle]}>
210
+ <Animated.View
211
+ style={[
212
+ styles.modalView,
213
+ {
214
+ backgroundColor,
215
+ borderRadius: isBottomSheet ? borderRadius : borderRadius,
216
+ ...(isBottomSheet && styles.modalViewBottomSheet),
217
+ },
218
+ modalAnimatedStyle,
219
+ modalStyle,
220
+ ]}
221
+ >
102
222
  {children}
103
223
  </Animated.View>
104
224
  </Animated.View>
@@ -112,25 +232,38 @@ const styles = StyleSheet.create({
112
232
  left: 0,
113
233
  right: 0,
114
234
  bottom: 0,
115
- // Changed to 'flex-end' to act like a bottom-sheet
116
235
  justifyContent: "center",
117
236
  alignItems: "center",
118
237
  zIndex: 1000,
119
238
  },
120
239
  backdrop: {
121
240
  ...StyleSheet.absoluteFillObject,
122
- backgroundColor: "rgba(0, 0, 0, 0.8)",
123
241
  },
124
242
  modalView: {
125
- // We keep your default styles
126
- // backgroundColor: 'white', // Good to apply this in the App.tsx
127
- borderTopLeftRadius: 10, // Added top radius for bottom-sheet look
128
- borderTopRightRadius: 10, // Added top radius for bottom-sheet look
129
- padding: 10,
243
+ padding: 20,
244
+ width: "90%",
245
+ maxWidth: 500,
246
+ maxHeight: "80%",
247
+ shadowColor: "#000",
248
+ shadowOffset: {
249
+ width: 0,
250
+ height: 2,
251
+ },
252
+ shadowOpacity: 0.25,
253
+ shadowRadius: 3.84,
254
+ elevation: 5,
255
+ },
256
+ modalViewBottomSheet: {
257
+ position: "absolute",
258
+ bottom: 0,
259
+ left: 0,
260
+ right: 0,
130
261
  width: "100%",
131
- maxHeight: "100%",
132
- // justifyContent: "center",
133
- // alignItems: "center",
262
+ maxWidth: "100%",
263
+ borderTopLeftRadius: 20,
264
+ borderTopRightRadius: 20,
265
+ borderBottomLeftRadius: 0,
266
+ borderBottomRightRadius: 0,
134
267
  },
135
268
  });
136
269