@space-uy/pulsar-ui 0.2.0

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.
Files changed (155) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +148 -0
  3. package/lib/module/components/Accordion.js +242 -0
  4. package/lib/module/components/Accordion.js.map +1 -0
  5. package/lib/module/components/BottomSheet.js +183 -0
  6. package/lib/module/components/BottomSheet.js.map +1 -0
  7. package/lib/module/components/Button.js +64 -0
  8. package/lib/module/components/Button.js.map +1 -0
  9. package/lib/module/components/ButtonContainer.js +118 -0
  10. package/lib/module/components/ButtonContainer.js.map +1 -0
  11. package/lib/module/components/CalendarPicker.js +374 -0
  12. package/lib/module/components/CalendarPicker.js.map +1 -0
  13. package/lib/module/components/Card.js +43 -0
  14. package/lib/module/components/Card.js.map +1 -0
  15. package/lib/module/components/Checkbox.js +122 -0
  16. package/lib/module/components/Checkbox.js.map +1 -0
  17. package/lib/module/components/Chip.js +50 -0
  18. package/lib/module/components/Chip.js.map +1 -0
  19. package/lib/module/components/CopyToClipboard.js +98 -0
  20. package/lib/module/components/CopyToClipboard.js.map +1 -0
  21. package/lib/module/components/Dialog.js +232 -0
  22. package/lib/module/components/Dialog.js.map +1 -0
  23. package/lib/module/components/Header.js +94 -0
  24. package/lib/module/components/Header.js.map +1 -0
  25. package/lib/module/components/Icon.js +22 -0
  26. package/lib/module/components/Icon.js.map +1 -0
  27. package/lib/module/components/IconButton.js +57 -0
  28. package/lib/module/components/IconButton.js.map +1 -0
  29. package/lib/module/components/Input.js +111 -0
  30. package/lib/module/components/Input.js.map +1 -0
  31. package/lib/module/components/InputContainer.js +104 -0
  32. package/lib/module/components/InputContainer.js.map +1 -0
  33. package/lib/module/components/LoadingIndicator.js +62 -0
  34. package/lib/module/components/LoadingIndicator.js.map +1 -0
  35. package/lib/module/components/OtpInput.js +85 -0
  36. package/lib/module/components/OtpInput.js.map +1 -0
  37. package/lib/module/components/OtpInputContainer.js +148 -0
  38. package/lib/module/components/OtpInputContainer.js.map +1 -0
  39. package/lib/module/components/Select.js +189 -0
  40. package/lib/module/components/Select.js.map +1 -0
  41. package/lib/module/components/Switch.js +74 -0
  42. package/lib/module/components/Switch.js.map +1 -0
  43. package/lib/module/components/Tabs.js +99 -0
  44. package/lib/module/components/Tabs.js.map +1 -0
  45. package/lib/module/components/Text.js +66 -0
  46. package/lib/module/components/Text.js.map +1 -0
  47. package/lib/module/components/TextArea.js +106 -0
  48. package/lib/module/components/TextArea.js.map +1 -0
  49. package/lib/module/hooks/useTheme.js +20 -0
  50. package/lib/module/hooks/useTheme.js.map +1 -0
  51. package/lib/module/index.js +27 -0
  52. package/lib/module/index.js.map +1 -0
  53. package/lib/module/package.json +1 -0
  54. package/lib/module/store/themeStore.js +50 -0
  55. package/lib/module/store/themeStore.js.map +1 -0
  56. package/lib/module/theme/colors.js +25 -0
  57. package/lib/module/theme/colors.js.map +1 -0
  58. package/lib/module/theme/meassures.js +10 -0
  59. package/lib/module/theme/meassures.js.map +1 -0
  60. package/lib/module/utils/stringUtils.js +12 -0
  61. package/lib/module/utils/stringUtils.js.map +1 -0
  62. package/lib/module/utils/uiUtils.js +63 -0
  63. package/lib/module/utils/uiUtils.js.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/components/Accordion.d.ts +22 -0
  66. package/lib/typescript/src/components/Accordion.d.ts.map +1 -0
  67. package/lib/typescript/src/components/BottomSheet.d.ts +13 -0
  68. package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -0
  69. package/lib/typescript/src/components/Button.d.ts +16 -0
  70. package/lib/typescript/src/components/Button.d.ts.map +1 -0
  71. package/lib/typescript/src/components/ButtonContainer.d.ts +30 -0
  72. package/lib/typescript/src/components/ButtonContainer.d.ts.map +1 -0
  73. package/lib/typescript/src/components/CalendarPicker.d.ts +19 -0
  74. package/lib/typescript/src/components/CalendarPicker.d.ts.map +1 -0
  75. package/lib/typescript/src/components/Card.d.ts +7 -0
  76. package/lib/typescript/src/components/Card.d.ts.map +1 -0
  77. package/lib/typescript/src/components/Checkbox.d.ts +11 -0
  78. package/lib/typescript/src/components/Checkbox.d.ts.map +1 -0
  79. package/lib/typescript/src/components/Chip.d.ts +9 -0
  80. package/lib/typescript/src/components/Chip.d.ts.map +1 -0
  81. package/lib/typescript/src/components/CopyToClipboard.d.ts +12 -0
  82. package/lib/typescript/src/components/CopyToClipboard.d.ts.map +1 -0
  83. package/lib/typescript/src/components/Dialog.d.ts +40 -0
  84. package/lib/typescript/src/components/Dialog.d.ts.map +1 -0
  85. package/lib/typescript/src/components/Header.d.ts +18 -0
  86. package/lib/typescript/src/components/Header.d.ts.map +1 -0
  87. package/lib/typescript/src/components/Icon.d.ts +12 -0
  88. package/lib/typescript/src/components/Icon.d.ts.map +1 -0
  89. package/lib/typescript/src/components/IconButton.d.ts +13 -0
  90. package/lib/typescript/src/components/IconButton.d.ts.map +1 -0
  91. package/lib/typescript/src/components/Input.d.ts +17 -0
  92. package/lib/typescript/src/components/Input.d.ts.map +1 -0
  93. package/lib/typescript/src/components/InputContainer.d.ts +22 -0
  94. package/lib/typescript/src/components/InputContainer.d.ts.map +1 -0
  95. package/lib/typescript/src/components/LoadingIndicator.d.ts +9 -0
  96. package/lib/typescript/src/components/LoadingIndicator.d.ts.map +1 -0
  97. package/lib/typescript/src/components/OtpInput.d.ts +3 -0
  98. package/lib/typescript/src/components/OtpInput.d.ts.map +1 -0
  99. package/lib/typescript/src/components/OtpInputContainer.d.ts +17 -0
  100. package/lib/typescript/src/components/OtpInputContainer.d.ts.map +1 -0
  101. package/lib/typescript/src/components/Select.d.ts +20 -0
  102. package/lib/typescript/src/components/Select.d.ts.map +1 -0
  103. package/lib/typescript/src/components/Switch.d.ts +10 -0
  104. package/lib/typescript/src/components/Switch.d.ts.map +1 -0
  105. package/lib/typescript/src/components/Tabs.d.ts +14 -0
  106. package/lib/typescript/src/components/Tabs.d.ts.map +1 -0
  107. package/lib/typescript/src/components/Text.d.ts +7 -0
  108. package/lib/typescript/src/components/Text.d.ts.map +1 -0
  109. package/lib/typescript/src/components/TextArea.d.ts +16 -0
  110. package/lib/typescript/src/components/TextArea.d.ts.map +1 -0
  111. package/lib/typescript/src/hooks/useTheme.d.ts +9 -0
  112. package/lib/typescript/src/hooks/useTheme.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +27 -0
  114. package/lib/typescript/src/index.d.ts.map +1 -0
  115. package/lib/typescript/src/store/themeStore.d.ts +32 -0
  116. package/lib/typescript/src/store/themeStore.d.ts.map +1 -0
  117. package/lib/typescript/src/theme/colors.d.ts +14 -0
  118. package/lib/typescript/src/theme/colors.d.ts.map +1 -0
  119. package/lib/typescript/src/theme/meassures.d.ts +9 -0
  120. package/lib/typescript/src/theme/meassures.d.ts.map +1 -0
  121. package/lib/typescript/src/utils/stringUtils.d.ts +7 -0
  122. package/lib/typescript/src/utils/stringUtils.d.ts.map +1 -0
  123. package/lib/typescript/src/utils/uiUtils.d.ts +21 -0
  124. package/lib/typescript/src/utils/uiUtils.d.ts.map +1 -0
  125. package/package.json +173 -0
  126. package/src/components/Accordion.tsx +284 -0
  127. package/src/components/BottomSheet.tsx +259 -0
  128. package/src/components/Button.tsx +85 -0
  129. package/src/components/ButtonContainer.tsx +161 -0
  130. package/src/components/CalendarPicker.tsx +428 -0
  131. package/src/components/Card.tsx +55 -0
  132. package/src/components/Checkbox.tsx +160 -0
  133. package/src/components/Chip.tsx +58 -0
  134. package/src/components/CopyToClipboard.tsx +108 -0
  135. package/src/components/Dialog.tsx +263 -0
  136. package/src/components/Header.tsx +100 -0
  137. package/src/components/Icon.tsx +27 -0
  138. package/src/components/IconButton.tsx +71 -0
  139. package/src/components/Input.tsx +144 -0
  140. package/src/components/InputContainer.tsx +134 -0
  141. package/src/components/LoadingIndicator.tsx +78 -0
  142. package/src/components/OtpInput.tsx +109 -0
  143. package/src/components/OtpInputContainer.tsx +196 -0
  144. package/src/components/Select.tsx +219 -0
  145. package/src/components/Switch.tsx +104 -0
  146. package/src/components/Tabs.tsx +117 -0
  147. package/src/components/Text.tsx +64 -0
  148. package/src/components/TextArea.tsx +141 -0
  149. package/src/hooks/useTheme.tsx +23 -0
  150. package/src/index.tsx +38 -0
  151. package/src/store/themeStore.ts +57 -0
  152. package/src/theme/colors.ts +35 -0
  153. package/src/theme/meassures.ts +7 -0
  154. package/src/utils/stringUtils.ts +16 -0
  155. package/src/utils/uiUtils.ts +70 -0
@@ -0,0 +1,284 @@
1
+ import React, { useState, useMemo, useEffect } from 'react';
2
+ import {
3
+ View,
4
+ Pressable,
5
+ StyleSheet,
6
+ type ViewStyle,
7
+ type StyleProp,
8
+ Platform,
9
+ type LayoutChangeEvent,
10
+ } from 'react-native';
11
+ import Animated, {
12
+ useSharedValue,
13
+ useAnimatedStyle,
14
+ withTiming,
15
+ interpolateColor,
16
+ interpolate,
17
+ } from 'react-native-reanimated';
18
+
19
+ import useTheme from '../hooks/useTheme';
20
+ import Text from './Text';
21
+ import Icon from './Icon';
22
+ import { convertHexToRgba } from '../utils/uiUtils';
23
+
24
+ export type AccordionItemProps = {
25
+ title: string;
26
+ children: React.ReactNode;
27
+ isExpanded?: boolean;
28
+ onToggle?: () => void;
29
+ style?: StyleProp<ViewStyle>;
30
+ value?: string;
31
+ };
32
+
33
+ export type AccordionProps = {
34
+ type?: 'single' | 'multiple';
35
+ collapsible?: boolean;
36
+ defaultValue?: string | string[];
37
+ value?: string | string[];
38
+ onValueChange?: (value: string | string[]) => void;
39
+ children: React.ReactNode;
40
+ style?: StyleProp<ViewStyle>;
41
+ };
42
+
43
+ export function AccordionItem({
44
+ title,
45
+ children,
46
+ isExpanded = false,
47
+ onToggle,
48
+ style,
49
+ }: AccordionItemProps) {
50
+ const { colors } = useTheme();
51
+ const pressed = useSharedValue(0);
52
+ const rotation = useSharedValue(0);
53
+ const height = useSharedValue(0);
54
+ const opacity = useSharedValue(0);
55
+ const [contentHeight, setContentHeight] = useState(0);
56
+
57
+ const itemStyle = useMemo(
58
+ () => ({
59
+ borderBottomWidth: 1,
60
+ borderBottomColor: colors.border,
61
+ backgroundColor: colors.background,
62
+ }),
63
+ [colors]
64
+ );
65
+
66
+ const pressedColor = convertHexToRgba(colors.border, 0.5);
67
+
68
+ const animatedTriggerStyle = useAnimatedStyle(() => {
69
+ return {
70
+ backgroundColor: interpolateColor(
71
+ pressed.value,
72
+ [0, 1],
73
+ [colors.background, pressedColor]
74
+ ),
75
+ };
76
+ });
77
+
78
+ const animatedChevronStyle = useAnimatedStyle(() => {
79
+ return {
80
+ transform: [
81
+ {
82
+ rotate: `${interpolate(rotation.value, [0, 1], [0, 180])}deg`,
83
+ },
84
+ ],
85
+ };
86
+ });
87
+
88
+ const animatedContentStyle = useAnimatedStyle(() => {
89
+ return {
90
+ height: height.value,
91
+ opacity: opacity.value,
92
+ };
93
+ });
94
+
95
+ const updatePressAnimation = (value: number) => {
96
+ pressed.value = withTiming(value, { duration: 200 });
97
+ };
98
+
99
+ const handlePressIn = () => {
100
+ Platform.OS !== 'web' && updatePressAnimation(1);
101
+ };
102
+
103
+ const handlePressOut = () => {
104
+ Platform.OS !== 'web' && updatePressAnimation(0);
105
+ };
106
+
107
+ useEffect(() => {
108
+ rotation.value = withTiming(isExpanded ? 1 : 0, { duration: 200 });
109
+ }, [isExpanded, rotation]);
110
+
111
+ useEffect(() => {
112
+ if (contentHeight > 0) {
113
+ height.value = withTiming(isExpanded ? contentHeight : 0, {
114
+ duration: 250,
115
+ });
116
+ opacity.value = withTiming(isExpanded ? 1 : 0, { duration: 200 });
117
+ }
118
+ }, [isExpanded, contentHeight, height, opacity]);
119
+
120
+ const handleContentLayout = (event: LayoutChangeEvent) => {
121
+ const { height: layoutHeight } = event.nativeEvent.layout;
122
+ if (layoutHeight > 0 && contentHeight !== layoutHeight) {
123
+ setContentHeight(layoutHeight);
124
+ if (isExpanded) {
125
+ height.value = layoutHeight;
126
+ opacity.value = 1;
127
+ } else {
128
+ height.value = 0;
129
+ opacity.value = 0;
130
+ }
131
+ }
132
+ };
133
+
134
+ return (
135
+ <View style={[itemStyle, style]}>
136
+ <Pressable
137
+ style={styles.trigger}
138
+ onPress={onToggle}
139
+ onPressIn={handlePressIn}
140
+ onPressOut={handlePressOut}
141
+ onHoverIn={() => updatePressAnimation(1)}
142
+ onHoverOut={() => updatePressAnimation(0)}
143
+ >
144
+ <Animated.View style={[styles.triggerContent, animatedTriggerStyle]}>
145
+ <Text
146
+ variant="h4"
147
+ style={[styles.title, { color: colors.foreground }]}
148
+ >
149
+ {title}
150
+ </Text>
151
+ <Animated.View style={[styles.chevron, animatedChevronStyle]}>
152
+ <Icon name="ChevronDown" size={20} color={colors.foreground} />
153
+ </Animated.View>
154
+ </Animated.View>
155
+ </Pressable>
156
+
157
+ <View>
158
+ <View style={styles.hiddenContent} onLayout={handleContentLayout}>
159
+ <View style={styles.content}>{children}</View>
160
+ </View>
161
+
162
+ {contentHeight > 0 && (
163
+ <Animated.View
164
+ style={[styles.contentContainer, animatedContentStyle]}
165
+ >
166
+ <View style={styles.content}>{children}</View>
167
+ </Animated.View>
168
+ )}
169
+ </View>
170
+ </View>
171
+ );
172
+ }
173
+
174
+ export default function Accordion({
175
+ type = 'single',
176
+ collapsible = false,
177
+ defaultValue,
178
+ value: controlledValue,
179
+ onValueChange,
180
+ children,
181
+ style,
182
+ }: AccordionProps) {
183
+ const { colors, theme } = useTheme();
184
+
185
+ const getInitialExpandedItems = (): Set<string> => {
186
+ if (controlledValue !== undefined) {
187
+ return new Set(
188
+ Array.isArray(controlledValue) ? controlledValue : [controlledValue]
189
+ );
190
+ }
191
+ if (defaultValue !== undefined) {
192
+ return new Set(
193
+ Array.isArray(defaultValue) ? defaultValue : [defaultValue]
194
+ );
195
+ }
196
+ return new Set();
197
+ };
198
+
199
+ const [expandedItems, setExpandedItems] = useState<Set<string>>(
200
+ getInitialExpandedItems
201
+ );
202
+
203
+ useEffect(() => {
204
+ if (controlledValue !== undefined) {
205
+ setExpandedItems(
206
+ new Set(
207
+ Array.isArray(controlledValue) ? controlledValue : [controlledValue]
208
+ )
209
+ );
210
+ }
211
+ }, [controlledValue]);
212
+
213
+ const toggleItem = (value: string) => {
214
+ setExpandedItems((prev) => {
215
+ const newSet = new Set(prev);
216
+
217
+ if (type === 'single') {
218
+ if (newSet.has(value)) {
219
+ if (collapsible) {
220
+ newSet.clear();
221
+ }
222
+ } else {
223
+ newSet.clear();
224
+ newSet.add(value);
225
+ }
226
+ } else if (newSet.has(value)) {
227
+ newSet.delete(value);
228
+ } else {
229
+ newSet.add(value);
230
+ }
231
+
232
+ const newValue =
233
+ type === 'single' ? Array.from(newSet)[0] || '' : Array.from(newSet);
234
+
235
+ onValueChange?.(newValue);
236
+ return newSet;
237
+ });
238
+ };
239
+
240
+ const accordionStyle = useMemo(
241
+ () => ({
242
+ backgroundColor: colors.background,
243
+ borderRadius: theme.roundness,
244
+ borderWidth: 1,
245
+ borderColor: colors.border,
246
+ overflow: 'hidden' as const,
247
+ }),
248
+ [colors, theme]
249
+ );
250
+
251
+ const accordionItems = React.Children.map(children, (child, index) => {
252
+ if (React.isValidElement(child)) {
253
+ const value = child.props.value || `item-${index}`;
254
+ return React.cloneElement(
255
+ child as React.ReactElement<AccordionItemProps>,
256
+ {
257
+ isExpanded: expandedItems.has(value),
258
+ onToggle: () => toggleItem(value),
259
+ key: value,
260
+ }
261
+ );
262
+ }
263
+ return child;
264
+ });
265
+
266
+ return <View style={[accordionStyle, style]}>{accordionItems}</View>;
267
+ }
268
+
269
+ const styles = StyleSheet.create({
270
+ trigger: { overflow: 'hidden' },
271
+ triggerContent: {
272
+ flexDirection: 'row',
273
+ alignItems: 'center',
274
+ justifyContent: 'space-between',
275
+ minHeight: 24,
276
+ paddingVertical: 16,
277
+ paddingHorizontal: 16,
278
+ },
279
+ chevron: { marginLeft: 8, justifyContent: 'center', alignItems: 'center' },
280
+ content: { paddingHorizontal: 16, paddingBottom: 16, overflow: 'hidden' },
281
+ contentContainer: { overflow: 'hidden' },
282
+ hiddenContent: { position: 'absolute', opacity: 0, zIndex: -1 },
283
+ title: { flex: 1 },
284
+ });
@@ -0,0 +1,259 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ type PropsWithChildren,
9
+ } from 'react';
10
+ import {
11
+ Modal,
12
+ StyleSheet,
13
+ TouchableWithoutFeedback,
14
+ useWindowDimensions,
15
+ View,
16
+ type LayoutChangeEvent,
17
+ } from 'react-native';
18
+ import Animated, {
19
+ Easing,
20
+ Extrapolation,
21
+ interpolate,
22
+ runOnJS,
23
+ useAnimatedStyle,
24
+ useSharedValue,
25
+ withTiming,
26
+ } from 'react-native-reanimated';
27
+ import {
28
+ Gesture,
29
+ GestureDetector,
30
+ GestureHandlerRootView,
31
+ } from 'react-native-gesture-handler';
32
+
33
+ import useTheme from '../hooks/useTheme';
34
+
35
+ import { convertHexToRgba } from '../utils/uiUtils';
36
+
37
+ type Props = PropsWithChildren & {
38
+ onBackdropPress?: () => void;
39
+ fullScreen?: boolean;
40
+ };
41
+
42
+ export type BottomSheetProps = {
43
+ show: () => void;
44
+ hide: () => void;
45
+ isActive: boolean;
46
+ };
47
+
48
+ const BottomSheet = forwardRef<BottomSheetProps, Props>(
49
+ (
50
+ { onBackdropPress = () => {}, children, fullScreen = false }: Props,
51
+ ref
52
+ ) => {
53
+ const [height, setHeight] = useState(0);
54
+ const [visible, setVisible] = useState(false);
55
+ const contentRef = useRef<View>(null);
56
+ const { colors, theme } = useTheme();
57
+ const { width: screenWidth, height: screenHeight } = useWindowDimensions();
58
+ const offset = useSharedValue(screenHeight);
59
+ const opacity = useSharedValue(0);
60
+
61
+ useImperativeHandle(ref, () => ({
62
+ show: () => setVisible(true),
63
+ hide: () => runAnimation(false),
64
+ isActive: visible,
65
+ }));
66
+
67
+ const transformAnimationStyle = useAnimatedStyle(() => {
68
+ return {
69
+ borderTopRightRadius: interpolate(
70
+ offset.value,
71
+ [0, screenHeight * 0.2],
72
+ [0, 20],
73
+ Extrapolation.CLAMP
74
+ ),
75
+ borderTopLeftRadius: interpolate(
76
+ offset.value,
77
+ [0, theme.insets.top + 150],
78
+ [0, 20],
79
+ Extrapolation.CLAMP
80
+ ),
81
+ transform: [{ translateY: offset.value }],
82
+ };
83
+ });
84
+
85
+ const handleContainerAnimStyle = useAnimatedStyle(() => {
86
+ return {
87
+ paddingTop: interpolate(
88
+ offset.value,
89
+ [0, 100],
90
+ [theme.insets.top + 16, 8],
91
+ Extrapolation.CLAMP
92
+ ),
93
+ };
94
+ });
95
+
96
+ const handleAnimStyle = useAnimatedStyle(() => {
97
+ return {
98
+ width: interpolate(
99
+ offset.value,
100
+ [0, 100],
101
+ [80, 40],
102
+ Extrapolation.CLAMP
103
+ ),
104
+ };
105
+ });
106
+
107
+ const opacityAnimatedStyle = useAnimatedStyle(() => {
108
+ return {
109
+ opacity: opacity.value,
110
+ };
111
+ });
112
+
113
+ const handleLayout = (e: LayoutChangeEvent) => {
114
+ setHeight(e.nativeEvent.layout.height);
115
+ };
116
+
117
+ const getAnimationValue = (
118
+ value: number,
119
+ easing: (value: number) => number,
120
+ closeOnFinish = false
121
+ ) => {
122
+ const config = { duration: 300, easing };
123
+ // If we don't do it like this, the app throw an error regarding de value returned
124
+ if (closeOnFinish) {
125
+ return withTiming(value, config, () => {
126
+ runOnJS(setVisible)(false);
127
+ });
128
+ }
129
+ return withTiming(value, config);
130
+ };
131
+
132
+ const runAnimation = useCallback(
133
+ (isOpenAnim: boolean) => {
134
+ const coordY = isOpenAnim ? screenHeight - height : screenHeight;
135
+ const easing = isOpenAnim
136
+ ? Easing.out(Easing.exp)
137
+ : Easing.in(Easing.exp);
138
+ offset.value = getAnimationValue(coordY, easing);
139
+ opacity.value = getAnimationValue(
140
+ isOpenAnim ? 1 : 0,
141
+ easing,
142
+ !isOpenAnim
143
+ );
144
+ },
145
+ [height, offset, opacity, screenHeight]
146
+ );
147
+
148
+ const panGesture = Gesture.Pan()
149
+ .onUpdate((event) => {
150
+ if (event.translationY > 0) {
151
+ offset.value = screenHeight - height + event.translationY;
152
+ }
153
+ })
154
+ .onEnd((event) => {
155
+ if (event.translationY > height * 0.3) {
156
+ offset.value = withTiming(screenHeight, { duration: 300 }, () => {
157
+ runOnJS(setVisible)(false);
158
+ runOnJS(onBackdropPress)();
159
+ });
160
+ opacity.value = withTiming(0, { duration: 300 });
161
+ } else {
162
+ offset.value = withTiming(screenHeight - height, { duration: 300 });
163
+ }
164
+ });
165
+
166
+ const handleBackdropPress = () => {
167
+ onBackdropPress?.();
168
+ runAnimation(false);
169
+ };
170
+
171
+ useEffect(() => {
172
+ if (visible && height > 0) {
173
+ runAnimation(true);
174
+ }
175
+ }, [height, runAnimation, visible]);
176
+
177
+ return (
178
+ /*
179
+ Need to wrap the modal with a View because there's a strange behavior in Android currently
180
+ which is not registering onPress events on components inside the modal. It seems to be something
181
+ related to the new architecture implementation on Andoid, change it once the issue is fixed.
182
+ See related issues:
183
+ - https://github.com/react-native-modal/react-native-modal/issues/737
184
+ - https://github.com/facebook/react-native/issues/36710
185
+ - https://github.com/facebook/react-native/issues/44643
186
+ */
187
+ <View>
188
+ <Modal
189
+ transparent
190
+ statusBarTranslucent
191
+ visible={visible}
192
+ onShow={() => runAnimation(true)}
193
+ >
194
+ {/*
195
+ Need to wrap with GestureHandlerRootView to make gesture handling work on Android.
196
+ This is required because React Native Gesture Handler needs a root view to properly
197
+ handle gestures on Android devices.
198
+ https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation#android
199
+ */}
200
+ <GestureHandlerRootView>
201
+ <TouchableWithoutFeedback onPress={handleBackdropPress}>
202
+ <Animated.View
203
+ style={[
204
+ StyleSheet.absoluteFill,
205
+ opacityAnimatedStyle,
206
+ { backgroundColor: convertHexToRgba(colors.foreground, 0.1) },
207
+ ]}
208
+ />
209
+ </TouchableWithoutFeedback>
210
+ <GestureDetector gesture={panGesture}>
211
+ <Animated.View
212
+ ref={contentRef}
213
+ onLayout={handleLayout}
214
+ style={[
215
+ styles.contentContainer,
216
+ { backgroundColor: colors.background, width: screenWidth },
217
+ transformAnimationStyle,
218
+ fullScreen
219
+ ? { height: screenHeight }
220
+ : { maxHeight: screenHeight * 0.8 },
221
+ ]}
222
+ >
223
+ <Animated.View
224
+ style={[
225
+ handleContainerAnimStyle,
226
+ { width: screenWidth },
227
+ styles.handleContainer,
228
+ ]}
229
+ >
230
+ <Animated.View
231
+ style={[
232
+ styles.handle,
233
+ { backgroundColor: colors.border },
234
+ handleAnimStyle,
235
+ ]}
236
+ />
237
+ </Animated.View>
238
+ {children}
239
+ </Animated.View>
240
+ </GestureDetector>
241
+ </GestureHandlerRootView>
242
+ </Modal>
243
+ </View>
244
+ );
245
+ }
246
+ );
247
+
248
+ const styles = StyleSheet.create({
249
+ container: { flex: 1 },
250
+ contentContainer: { overflow: 'hidden' },
251
+ handle: { height: 8, borderRadius: 8, alignSelf: 'center' },
252
+ handleContainer: {
253
+ justifyContent: 'flex-end',
254
+ alignItems: 'center',
255
+ paddingBottom: 16,
256
+ },
257
+ });
258
+
259
+ export default BottomSheet;
@@ -0,0 +1,85 @@
1
+ import { StyleSheet, type ViewStyle, type StyleProp } from 'react-native';
2
+
3
+ import ButtonContainer, {
4
+ type ButtonVariant,
5
+ type ButtonSize,
6
+ type ButtonColors,
7
+ } from './ButtonContainer';
8
+ import LoadingIndicator from './LoadingIndicator';
9
+ import Text from './Text';
10
+ import Icon, { type IconName } from './Icon';
11
+
12
+ type Props = {
13
+ style?: StyleProp<ViewStyle>;
14
+ text: string;
15
+ variant?: keyof typeof ButtonVariant;
16
+ size?: keyof typeof ButtonSize;
17
+ loading?: boolean;
18
+ disabled?: boolean;
19
+ onPress?: () => void;
20
+ iconName?: IconName;
21
+ };
22
+
23
+ export default function Button({
24
+ text,
25
+ style,
26
+ variant = 'flat',
27
+ size = 'large',
28
+ loading = false,
29
+ disabled = false,
30
+ onPress,
31
+ iconName,
32
+ }: Props) {
33
+ const renderIconOrLoader = (colors: ButtonColors) => {
34
+ if (loading) {
35
+ return (
36
+ <LoadingIndicator
37
+ style={styles.icon}
38
+ color={colors.textColor}
39
+ size={size === 'small' ? 14 : 16}
40
+ />
41
+ );
42
+ }
43
+ return iconName ? (
44
+ <Icon
45
+ style={styles.icon}
46
+ name={iconName}
47
+ size={16}
48
+ color={colors.textColor}
49
+ />
50
+ ) : null;
51
+ };
52
+
53
+ return (
54
+ <ButtonContainer
55
+ style={style}
56
+ contentContainerStyle={styles.content}
57
+ variant={variant}
58
+ size={size}
59
+ loading={loading}
60
+ disabled={disabled}
61
+ onPress={onPress}
62
+ renderContent={(colors: ButtonColors) => (
63
+ <>
64
+ {renderIconOrLoader(colors)}
65
+ <Text
66
+ style={{ color: colors.textColor }}
67
+ variant={size === 'small' ? 'h5' : 'h4'}
68
+ >
69
+ {text}
70
+ </Text>
71
+ </>
72
+ )}
73
+ />
74
+ );
75
+ }
76
+
77
+ const styles = StyleSheet.create({
78
+ content: {
79
+ paddingHorizontal: 16,
80
+ flexDirection: 'row',
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ },
84
+ icon: { marginRight: 8 },
85
+ });