ripal-ui 2.0.0 → 2.0.2

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
@@ -1,3 +1,3 @@
1
1
  # Ripal UI
2
2
 
3
- The doc is working in progress
3
+ The doc is working in progress (still)
@@ -54,7 +54,7 @@ const Alert = ({title, body = null, color = COLORS.primary, icon = null, iconSiz
54
54
  const styles = StyleSheet.create({
55
55
  container: {
56
56
  borderWidth: 1,
57
- borderColor: '#ddd',
57
+ borderColor: COLORS.slate[200],
58
58
  borderRadius: 20,
59
59
  padding: 10,
60
60
  },
@@ -3,19 +3,6 @@ import Inline from "./Inline";
3
3
  import { Image, StyleSheet, View } from "react-native";
4
4
  import Text from "./Text";
5
5
 
6
- /**
7
- * @typedef {Object} AvatarProps
8
- * @property {string|array} [images] - URI of Avatars image
9
- * @property {int|null} [size] - Size of the image
10
- * @property {int|null} [radius] - Rounded level of avatar image (borderRadius)
11
- * @property {string|null} [borderColor] - Edge color of each image
12
- * @property {int|null} [borderWidth] - Border with of each image
13
- * @property {int|null} [limit] - Number of displayed image as in a group
14
- */
15
-
16
- /**
17
- * @param {AvatarProps} props
18
- */
19
6
  const Avatar = ({
20
7
  images,
21
8
  size = 48,
@@ -0,0 +1,187 @@
1
+ // BottomSheet.jsx
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import {
4
+ Animated, ScrollView, StyleSheet, TouchableWithoutFeedback, Platform, Pressable,
5
+ KeyboardAvoidingView,
6
+ Keyboard,
7
+ } from "react-native";
8
+
9
+ const OFFSCREEN = 1500;
10
+ const CLOSE_THRESHOLD = -100;
11
+
12
+ const BottomSheet = ({ visible = false, setVisible = null, onClose = null, children, style = null }) => {
13
+ const [isClosing, setClosing] = useState(false);
14
+ const [isVisible, setIsVisible] = useState(visible);
15
+
16
+ useEffect(() => {
17
+ if (!visible) return;
18
+
19
+ translateY.setValue(OFFSCREEN);
20
+
21
+ Animated.timing(translateY, {
22
+ toValue: 0,
23
+ duration: 300,
24
+ useNativeDriver: true,
25
+ }).start();
26
+
27
+ setClosing(false);
28
+
29
+ }, [visible]);
30
+
31
+ const translateY = useRef(new Animated.Value(OFFSCREEN)).current;
32
+ const keyboardShowEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
33
+ const keyboardHideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
34
+
35
+ const backdropOpacity = translateY.interpolate({
36
+ inputRange: [0, 300],
37
+ outputRange: [0.4, 0],
38
+ extrapolate: "clamp",
39
+ });
40
+
41
+ useEffect(() => {
42
+ const keyboardDidShowListener = Keyboard.addListener(keyboardShowEvent, event => {
43
+ let keyboardHeight = event.endCoordinates.height - 81;
44
+
45
+ Animated.timing(translateY, {
46
+ toValue: -keyboardHeight,
47
+ duration: 300,
48
+ useNativeDriver: true,
49
+ }).start();
50
+ });
51
+ const keyboardDidHideListener = Keyboard.addListener(keyboardHideEvent, event => {
52
+ Animated.timing(translateY, {
53
+ toValue: 0,
54
+ duration: 300,
55
+ useNativeDriver: true,
56
+ }).start();
57
+ });
58
+
59
+ return () => {
60
+ keyboardDidShowListener.remove();
61
+ keyboardDidHideListener.remove();
62
+ }
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ translateY.setValue(OFFSCREEN);
67
+ Animated.timing(translateY, {
68
+ toValue: 0,
69
+ duration: 300,
70
+ useNativeDriver: true,
71
+ }).start();
72
+
73
+ setClosing(false);
74
+ }, []);
75
+
76
+ // animate out when isClosing turns true
77
+ useEffect(() => {
78
+ if (!isClosing) return;
79
+
80
+ Animated.timing(translateY, {
81
+ toValue: OFFSCREEN,
82
+ duration: 300,
83
+ useNativeDriver: true,
84
+ }).start(() => {
85
+ // after closing animation: tell parent to remove this component
86
+ if (typeof onClose === "function") {
87
+ onClose();
88
+ }
89
+ if (setVisible) {
90
+ setVisible(false);
91
+ }
92
+ // just in case it's not unmounted immediately, reset state/value
93
+ setClosing(false);
94
+ translateY.setValue(OFFSCREEN);
95
+ });
96
+ }, [isClosing]);
97
+
98
+ // handle backdrop press
99
+ const handleBackdropPress = () => {
100
+
101
+ if (!isClosing) {
102
+ setClosing(true);
103
+ };
104
+ };
105
+
106
+ if (!visible) return null;
107
+
108
+ return (
109
+ <Pressable style={styles.wrapper} pointerEvents="box-none" onPress={handleBackdropPress}>
110
+ {/* Animated backdrop */}
111
+ <TouchableWithoutFeedback onPress={handleBackdropPress}>
112
+ <Animated.View
113
+ style={[
114
+ styles.backdrop,
115
+ { opacity: backdropOpacity },
116
+ ]}
117
+ />
118
+ </TouchableWithoutFeedback>
119
+
120
+ <KeyboardAvoidingView
121
+ behavior={Platform.OS === "ios" ? "padding" : undefined}
122
+ keyboardVerticalOffset={Platform.OS === "ios" ? 60 : 0} // tweak if needed
123
+ style={{ flex: 1, justifyContent: "flex-end" }}
124
+ >
125
+ <Animated.View
126
+ style={[
127
+ styles.container_outer,
128
+ { transform: [
129
+ { translateY },
130
+ ] },
131
+ ]}
132
+ >
133
+ <ScrollView
134
+ contentContainerStyle={{ flexGrow: 1, justifyContent: 'flex-end', borderRadius: 12 }}
135
+ scrollEventThrottle={16}
136
+ // allow bigger overscroll on Android/iOS
137
+ bounces={true}
138
+ overScrollMode={Platform.OS === "android" ? "always" : undefined}
139
+ onScroll={(event) => {
140
+ const pos = event.nativeEvent.contentOffset.y;
141
+ // if user pulls down far enough, start closing
142
+ if (pos < CLOSE_THRESHOLD && !isClosing) {
143
+ setClosing(true);
144
+ }
145
+ }}
146
+ nestedScrollEnabled
147
+ >
148
+ <Pressable style={[styles.container, style]}>
149
+ {children}
150
+ </Pressable>
151
+ </ScrollView>
152
+ </Animated.View>
153
+ </KeyboardAvoidingView>
154
+ </Pressable>
155
+ );
156
+ };
157
+
158
+ const styles = StyleSheet.create({
159
+ wrapper: {
160
+ ...StyleSheet.absoluteFillObject,
161
+ zIndex: 999,
162
+ justifyContent: "flex-end",
163
+ },
164
+ backdrop: {
165
+ ...StyleSheet.absoluteFillObject,
166
+ backgroundColor: "#000",
167
+ },
168
+ container_outer: {
169
+ position: "absolute",
170
+ left: 0,
171
+ right: 0,
172
+ bottom: 0,
173
+ top: 0,
174
+ paddingTop: 90, // same as your earlier paddingTop
175
+ justifyContent: "flex-end",
176
+ alignContent: "flex-end",
177
+ },
178
+ container: {
179
+ backgroundColor: "#fff",
180
+ padding: 20,
181
+ paddingVertical: 30,
182
+ borderTopLeftRadius: 12,
183
+ borderTopRightRadius: 12,
184
+ },
185
+ });
186
+
187
+ export default BottomSheet;
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { StyleSheet, TouchableOpacity } from "react-native";
3
+ import Inline from "./Inline";
4
+ import Text from "./Text";
5
+ import MaterialIcons from "@react-native-vector-icons/material-icons";
6
+ import COLORS from "./COLORS";
7
+
8
+ const Breadcrumb = ({items, gap = 5, icon = null}) => {
9
+ const isLast = (index) => index === items.length - 1;
10
+ return (
11
+ <Inline gap={gap}>
12
+ { items.map((item, i) => (
13
+ <Inline key={i} gap={gap}>
14
+ <TouchableOpacity key={i} style={[styles.item, isLast(i) ? styles.item_active : null]} onPress={() => {
15
+ if (item.hasOwnProperty('onPress')) {
16
+ item.onPress(item);
17
+ }
18
+ }}>
19
+ <Text
20
+ size={13}
21
+ color={isLast(i) ? COLORS.primary : COLORS.slate[700]}
22
+ style={{ fontWeight: isLast(i) ? '600' : '400' }}
23
+ >{item.text}</Text>
24
+ </TouchableOpacity>
25
+ {
26
+ i !== items.length - 1 &&
27
+ <>
28
+ {icon ? icon : <MaterialIcons name="chevron-right" size={14} />}
29
+ </>
30
+ }
31
+ </Inline>
32
+ )) }
33
+ </Inline>
34
+ )
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ item: {
39
+ borderRadius: 8,
40
+ },
41
+ item_active: {
42
+ padding: 8,
43
+ paddingHorizontal: 12,
44
+ backgroundColor: `${COLORS.primary}20`,
45
+ }
46
+ })
47
+
48
+ export default Breadcrumb;
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { ImageBackground, StyleSheet, View } from "react-native";
3
+ import Text from "./Text";
4
+ import COLORS from "./COLORS";
5
+
6
+ const Card = ({children, image = null, radius = 15, style, imageStyle}) => {
7
+ return (
8
+ <ImageBackground source={{uri: image}} imageStyle={[
9
+ {
10
+ borderRadius: radius,
11
+ },
12
+ imageStyle
13
+ ]} style={[
14
+ styles.card,
15
+ {
16
+ borderRadius: radius,
17
+ },
18
+ style
19
+ ]}>
20
+ {children}
21
+ </ImageBackground>
22
+ )
23
+ }
24
+
25
+ const styles = StyleSheet.create({
26
+ card: {
27
+ padding: 20,
28
+ borderWidth: 1,
29
+ borderColor: COLORS.slate[200]
30
+ }
31
+ })
32
+
33
+ export default Card;
@@ -3,26 +3,14 @@ import { StyleSheet, TouchableOpacity } from "react-native";
3
3
  import COLORS from "./COLORS";
4
4
  import MaterialIcons from "@react-native-vector-icons/material-icons";
5
5
 
6
- /**
7
- * @typedef {Object} CheckboxProps
8
- * @property {int} [size] - Checkbox's outer size
9
- * @property {boolean} [active] - State to control mark's visibility (useState)
10
- * @property {void} [setActive] - Function to mutate the state (useState)
11
- * @property {null} [icon] - Icon component to override the default checkmark icon
12
- * @property {int|null} [iconSize] - Size of default icon (checkmark)
13
- */
14
-
15
- /**
16
- * @param {CheckboxProps} props
17
- */
18
- const Checkbox = ({size = 20, active, setActive, icon = null, iconSize = 14}) => {
6
+ const Checkbox = ({size = 20, active, color = COLORS.primary, setActive, icon = null, iconSize = 14}) => {
19
7
  return (
20
8
  <TouchableOpacity style={[
21
9
  styles.box,
22
10
  {
23
11
  height: size,
24
- borderColor: active ? COLORS.primary : COLORS.slate[300],
25
- backgroundColor: active ? COLORS.primary : '#ffffff00'
12
+ borderColor: active ? color : COLORS.slate[300],
13
+ backgroundColor: active ? color : '#ffffff00'
26
14
  }
27
15
  ]} onPress={() => {
28
16
  setActive(!active);
@@ -5,27 +5,6 @@ import Inline from "./Inline";
5
5
  import COLORS from "./COLORS";
6
6
  import Button from "./Button";
7
7
  import MaterialIcons from "@react-native-vector-icons/material-icons";
8
- /**
9
- * @typedef {Object} DialogProps
10
- * @property {boolean} [visible] - State to control dialog visibility (useState)
11
- * @property {void} [setVisible] - Function to mutate the visible state (useState)
12
- * @property {void} [onClose] - Close event
13
- */
14
-
15
- /**
16
- * @typedef {Object} BodyProps
17
- * @property {int|null} [gap] - Flex gap inside the dialog's body
18
- */
19
-
20
- /**
21
- * @typedef {Object} TitleProps
22
- * @property {bool|null} [withClose] - Set to false to hide close button
23
- */
24
-
25
- /**
26
- * @typedef {Object} ActionProps
27
- * @property {int|null} [gap] - Flex gap inside the dialog's body
28
- */
29
8
 
30
9
  const DialogContext = createContext(null);
31
10
 
@@ -35,9 +14,6 @@ const useDialog = () => {
35
14
  return ctx;
36
15
  };
37
16
 
38
- /**
39
- * @param {DialogProps} props
40
- */
41
17
  const Dialog = ({
42
18
  children,
43
19
  visible = false,
@@ -109,9 +85,6 @@ const Dialog = ({
109
85
  );
110
86
  };
111
87
 
112
- /**
113
- * @param {BodyProps} props
114
- */
115
88
  Dialog.Body = ({ children, gap = 20 }) => {
116
89
  const { close } = useDialog();
117
90
 
@@ -122,9 +95,6 @@ Dialog.Body = ({ children, gap = 20 }) => {
122
95
  )
123
96
  }
124
97
 
125
- /**
126
- * @param {TitleProps} props
127
- */
128
98
  Dialog.Title = ({ children, withClose = true }) => {
129
99
  const { close } = useDialog();
130
100
 
@@ -161,9 +131,6 @@ Dialog.Title = ({ children, withClose = true }) => {
161
131
  );
162
132
  };
163
133
 
164
- /**
165
- * @param {ActionProps} props
166
- */
167
134
  Dialog.Actions = ({ children, gap = 10 }) => {
168
135
  return (
169
136
  <Inline style={[styles.action_area]} gap={gap}>
@@ -0,0 +1,26 @@
1
+ import React, { useEffect, useState, memo } from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+
4
+ const Grid = ({ children, col = 2, gap = 20, style }) => {
5
+ const [containerWidth, setContainerWidth] = useState(0);
6
+ const [itemWidth, setItemWidth] = useState(0);
7
+ const handleLayout = (event) => {
8
+ const newWidth = event.nativeEvent.layout.width;
9
+ setContainerWidth(prev => (prev !== newWidth ? newWidth : prev));
10
+ };
11
+ useEffect(() => {
12
+ if (containerWidth > 0 && col > 0) {
13
+ const totalGap = gap * (col - 1);
14
+ const calculatedWidth = (containerWidth - totalGap) / col;
15
+ setItemWidth(prev => prev !== calculatedWidth ? calculatedWidth : prev);
16
+ }
17
+ }, [containerWidth, col, gap]);
18
+ return (React.createElement(View, { style: [styles.container, { gap }, style], onLayout: handleLayout }, React.Children.map(children, (child, index) => (React.createElement(View, { key: index, style: { width: itemWidth } }, child)))));
19
+ };
20
+ const styles = StyleSheet.create({
21
+ container: {
22
+ flexDirection: "row",
23
+ flexWrap: "wrap"
24
+ }
25
+ });
26
+ export default memo(Grid);
@@ -0,0 +1,65 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+ import COLORS from "./COLORS";
4
+ import Text from "./Text";
5
+ import Inline from "./Inline";
6
+
7
+ const ProgressBar = ({value, label = null, displayValue = true, labelPosition = 'left', from = null, color = COLORS.primary, height = 24, radius = 999}) => {
8
+ const [width, setWidth] = useState(0);
9
+
10
+ useEffect(() => {
11
+ if (from === null) {
12
+ setWidth(value);
13
+ } else {
14
+ let percentage = value / from * 100;
15
+ setWidth(percentage);
16
+ }
17
+ }, [value, from]);
18
+
19
+ return (
20
+ <View style={{ gap: 10 }}>
21
+ {
22
+ (label !== null) &&
23
+ <Inline justifyContent={labelPosition?.toLowerCase() === "left" ? 'flex-start' : 'flex-end'}>
24
+ {typeof label === "string" ? <Text>{label}</Text> : label}
25
+ </Inline>
26
+ }
27
+ <View style={[
28
+ styles.container,
29
+ {
30
+ minHeight: height,
31
+ backgroundColor: `${color}20`,
32
+ borderRadius: radius,
33
+ }
34
+ ]}>
35
+ <Inline justifyContent={'flex-end'} style={[
36
+ styles.bar,
37
+ {
38
+ minHeight: height,
39
+ backgroundColor: color,
40
+ borderRadius: radius,
41
+ width: `${width}%`,
42
+ paddingHorizontal: displayValue ? 10 : 0,
43
+ // padding: displayValue ? 5 : 0,
44
+ }
45
+ ]}>
46
+ {
47
+ displayValue &&
48
+ <Text color="#fff" style={{fontWeight: '700'}}>{value}%</Text>
49
+ }
50
+ </Inline>
51
+ </View>
52
+ </View>
53
+ )
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ //
59
+ },
60
+ bar: {
61
+ //
62
+ }
63
+ });
64
+
65
+ export default ProgressBar
package/index.js CHANGED
@@ -1,13 +1,18 @@
1
1
  export { default as Alert } from "./components/Alert";
2
2
  export { default as Avatar } from "./components/Avatar";
3
+ export { default as BottomSheet } from "./components/BottomSheet";
4
+ export { default as Breadcrumb } from "./components/Breadcrumb";
3
5
  export { default as Button } from "./components/Button";
6
+ export { default as Card } from "./components/Card";
4
7
  export { default as Checkbox } from "./components/Checkbox";
5
8
  export { default as COLORS } from "./components/COLORS";
6
9
  export { default as Dialog } from "./components/Dialog";
7
10
  export { default as Divider } from "./components/Divider";
8
11
  export { default as Dropdown } from "./components/Dropdown";
12
+ export { default as Grid } from "./components/Grid";
9
13
  export { default as Inline } from "./components/Inline";
10
14
  export { default as Input } from "./components/Input";
15
+ export { default as ProgressBar } from "./components/ProgressBar";
11
16
  export { default as Rate } from "./components/Rate";
12
17
  export { default as Slider } from "./components/Slider";
13
18
  export { default as Switch } from "./components/Switch";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ripal-ui",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Reusable React Native UI components",
5
5
  "main": "index.js",
6
6
  "react-native": "index.js",