nx-react-native-cli 2.7.1 → 3.0.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 (56) hide show
  1. package/lib/index.cjs +27 -27
  2. package/package.json +1 -1
  3. package/templates/shared/apps/mobile/src/app/index.tsx +13 -8
  4. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.component.tsx +134 -0
  5. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.types.ts +18 -0
  6. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert.service.ts +27 -0
  7. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/index.ts +3 -0
  8. package/templates/shared/apps/mobile/src/components/atoms/BottomSheet/bottom-sheet.component.tsx +14 -8
  9. package/templates/shared/apps/mobile/src/components/atoms/Button/button.component.tsx +1 -1
  10. package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/date-modal-input.component.tsx +69 -0
  11. package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/index.ts +1 -0
  12. package/templates/shared/apps/mobile/src/components/atoms/DatePicker/date-picker.component.tsx +44 -0
  13. package/templates/shared/apps/mobile/src/components/atoms/DatePicker/index.ts +1 -0
  14. package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/date-text-input.component.tsx +218 -0
  15. package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/index.ts +1 -0
  16. package/templates/shared/apps/mobile/src/components/atoms/Divider/divider-component.tsx +1 -1
  17. package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/gradient-background.component.tsx +45 -0
  18. package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/index.ts +1 -0
  19. package/templates/shared/apps/mobile/src/components/atoms/InputLayout/input-layout.component.tsx +12 -4
  20. package/templates/shared/apps/mobile/src/components/atoms/KeyboardAccessory/keyboard-accessory.component.tsx +6 -3
  21. package/templates/shared/apps/mobile/src/components/atoms/Modal/modal.component.tsx +2 -0
  22. package/templates/shared/apps/mobile/src/components/atoms/ScreenLoader/screen-loader.component.tsx +6 -1
  23. package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/index.ts +1 -0
  24. package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/select-dropdown.component.tsx +223 -0
  25. package/templates/shared/apps/mobile/src/components/atoms/Skeleton/skeleton.component.tsx +1 -1
  26. package/templates/shared/apps/mobile/src/components/atoms/TextInput/bottom-sheet-text-input.component.tsx +4 -3
  27. package/templates/shared/apps/mobile/src/components/atoms/TextInput/text-input.component.tsx +8 -4
  28. package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/index.ts +1 -0
  29. package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/theme-manager.component.tsx +27 -0
  30. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/index.ts +3 -0
  31. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.component.tsx +109 -0
  32. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.types.ts +10 -0
  33. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast.service.ts +27 -0
  34. package/templates/shared/apps/mobile/src/components/atoms/Typography/typography.component.tsx +1 -1
  35. package/templates/shared/apps/mobile/src/components/atoms/index.ts +8 -0
  36. package/templates/shared/apps/mobile/src/components/molecules/BackButton/back-button.component.tsx +1 -1
  37. package/templates/shared/apps/mobile/src/components/molecules/ScreenContainer/screen-container.component.tsx +2 -22
  38. package/templates/shared/apps/mobile/src/components/molecules/ScreenHeader/screen-header.component.tsx +2 -2
  39. package/templates/shared/apps/mobile/src/hooks/index.ts +1 -0
  40. package/templates/shared/apps/mobile/src/hooks/usePushNotifications.hook.ts +104 -0
  41. package/templates/shared/apps/mobile/src/hooks/useToggleDarkMode.hook.tsx +24 -2
  42. package/templates/shared/apps/mobile/src/icons/alert-triangle.svg +5 -0
  43. package/templates/shared/apps/mobile/src/icons/check-circle.svg +4 -0
  44. package/templates/shared/apps/mobile/src/icons/chevron-down.svg +1 -0
  45. package/templates/shared/apps/mobile/src/icons/chevron-right.svg +1 -0
  46. package/templates/shared/apps/mobile/src/icons/index.ts +18 -1
  47. package/templates/shared/apps/mobile/src/icons/info.svg +5 -0
  48. package/templates/shared/apps/mobile/src/icons/x-circle.svg +5 -0
  49. package/templates/shared/apps/mobile/src/routes/index.tsx +19 -14
  50. package/templates/shared/apps/mobile/src/screens/LandingScreen/landing.screen.tsx +232 -8
  51. package/templates/shared/apps/mobile/src/stores/local-storage.store.ts +9 -5
  52. package/templates/shared/apps/mobile/src/stores/theme.slice.ts +15 -0
  53. package/templates/shared/apps/mobile/src/stores/user.slice.ts +5 -1
  54. package/templates/shared/apps/mobile/src/tailwind/index.ts +3 -3
  55. package/templates/shared/apps/mobile/tailwind.config.js +14 -0
  56. package/templates/shared/patches/react-native-animatable+1.4.0.patch +71 -0
@@ -0,0 +1,45 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ import { Canvas, Fill, LinearGradient, vec } from '@shopify/react-native-skia';
3
+ import React from 'react';
4
+ import { StyleSheet, useWindowDimensions } from 'react-native';
5
+
6
+ import { useLocalStorageStore } from '@/stores';
7
+ import { colors } from '@/tailwind';
8
+
9
+ type Props = {
10
+ variant?: 'bottomSheet' | 'login' | 'welcome';
11
+ };
12
+
13
+ const DARK_GRADIENTS: Record<string, string[]> = {
14
+ bottomSheet: [colors.sheet, colors.background],
15
+ login: [colors.secondary[950], colors.background, colors.secondary[950]],
16
+ welcome: [colors.background, colors.secondary[950], colors.background],
17
+ };
18
+
19
+ const LIGHT_GRADIENTS: Record<string, string[]> = {
20
+ bottomSheet: [colors.gray[100], colors.white],
21
+ login: [colors.gray[50], colors.white, colors.gray[50]],
22
+ welcome: [colors.white, colors.gray[50], colors.white],
23
+ };
24
+
25
+ export function GradientBackground(props: Props) {
26
+ const { variant = 'login' } = props;
27
+ const colorScheme = useLocalStorageStore((s) => s.colorScheme);
28
+ const isDark = colorScheme === 'dark';
29
+ const { height, width } = useWindowDimensions();
30
+
31
+ const gradientColors = isDark ? DARK_GRADIENTS[variant] : LIGHT_GRADIENTS[variant];
32
+
33
+ return (
34
+ <Canvas style={StyleSheet.absoluteFill}>
35
+ {/* Base linear gradient */}
36
+ <Fill>
37
+ <LinearGradient
38
+ colors={gradientColors}
39
+ end={vec(width / 2, height)}
40
+ start={vec(width / 2, 0)}
41
+ />
42
+ </Fill>
43
+ </Canvas>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export * from './gradient-background.component';
@@ -9,24 +9,32 @@ type Props = DefaultComponentProps & {
9
9
  children?: React.ReactNode;
10
10
  isRequired?: boolean;
11
11
  label?: string;
12
+ subtitle?: string;
12
13
  textStyle?: StyleProp<TextStyle>;
13
14
  };
14
15
 
15
16
  export function InputLayout(props: Props) {
16
- const { children, error, isRequired, label, style, textStyle } = props;
17
+ const { children, error, isRequired, label, style, subtitle, textStyle } = props;
17
18
 
18
19
  return (
19
20
  <View style={[style]}>
20
21
  {label && (
21
- <Typography style={[tw`mb-2 text-gray-600`, textStyle]}>
22
+ <Typography style={[tw`mb-2 text-gray-600 dark:text-gray-300`, textStyle]}>
22
23
  {label}
23
24
  {isRequired && <Typography style={tw`text-red-600`}>{isRequired && '*'}</Typography>}
24
25
  </Typography>
25
26
  )}
26
27
  {children}
27
28
  {!!error && (
28
- <View style={tw`items-end`}>
29
- <Typography style={tw`text-right text-red-500`}>{error}</Typography>
29
+ <View style={tw`mt-1 items-start`}>
30
+ <Typography style={tw`text-right text-xs text-red-500`}>{error}</Typography>
31
+ </View>
32
+ )}
33
+ {!!subtitle && !error && (
34
+ <View style={tw`mt-1 items-start`}>
35
+ <Typography style={tw`dark:text-subtitle text-right text-xs text-gray-400`}>
36
+ {subtitle}
37
+ </Typography>
30
38
  </View>
31
39
  )}
32
40
  </View>
@@ -15,9 +15,12 @@ export function KeyboardAccessory(props: Props) {
15
15
 
16
16
  return (
17
17
  <InputAccessoryView nativeID={nativeID}>
18
- <View style={tw`flex-row items-center justify-end bg-[#313132] px-2`}>
19
- <Button buttonStyle={tw`my-2 rounded-lg bg-[#717172]`} onPress={() => Keyboard.dismiss()}>
20
- <KeyboardHideIcon style={tw`text-white`} />
18
+ <View style={tw`flex-row items-center justify-end bg-gray-100 px-2 dark:bg-[#313132]`}>
19
+ <Button
20
+ buttonStyle={tw`my-2 rounded-lg bg-gray-200 dark:bg-[#717172]`}
21
+ onPress={() => Keyboard.dismiss()}
22
+ >
23
+ <KeyboardHideIcon style={tw`text-gray-600 dark:text-white`} />
21
24
  </Button>
22
25
  </View>
23
26
  </InputAccessoryView>
@@ -30,6 +30,8 @@ function IOSModal(props: ModalProps) {
30
30
 
31
31
  return (
32
32
  <RNModal
33
+ hideModalContentWhileAnimating
34
+ useNativeDriver
33
35
  animationIn="fadeIn"
34
36
  animationInTiming={FADE_IN_DURATION}
35
37
  animationOut="fadeOut"
@@ -10,7 +10,12 @@ export function ScreenLoader(props: Props) {
10
10
  const { style } = props;
11
11
 
12
12
  return (
13
- <View style={[tw`h-full w-full items-center justify-center bg-gray-50 p-8`, style]}>
13
+ <View
14
+ style={[
15
+ tw`dark:bg-background h-full w-full items-center justify-center bg-gray-50 p-8`,
16
+ style,
17
+ ]}
18
+ >
14
19
  <ActivityIndicator color={colors.primary[400]} />
15
20
  </View>
16
21
  );
@@ -0,0 +1 @@
1
+ export * from './select-dropdown.component';
@@ -0,0 +1,223 @@
1
+ /* eslint-disable max-params */
2
+ /* eslint-disable no-magic-numbers */
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Dimensions, Pressable, ScrollView, View } from 'react-native';
5
+
6
+ import { Modal, Typography } from '@/components';
7
+ import { ChevronDownIcon, ChevronRightIcon } from '@/icons';
8
+ import { colors, defaultInputContainerStyle, defaultInputTextStyle, tw } from '@/tailwind';
9
+ import { DefaultComponentProps } from '@/types';
10
+
11
+ const DROPDOWN_OFFSET = 8;
12
+ const MAX_MENU_HEIGHT = 320;
13
+ const OPTION_ROW_HEIGHT = 48;
14
+
15
+ type DropdownOption = {
16
+ id: string;
17
+ label: string;
18
+ };
19
+
20
+ type DropdownLayout = {
21
+ height: number;
22
+ width: number;
23
+ x: number;
24
+ y: number;
25
+ };
26
+
27
+ type Props = DefaultComponentProps & {
28
+ label?: string;
29
+ options: DropdownOption[];
30
+ placeholder?: string;
31
+ selectedId?: string;
32
+ onSelect: (id: string) => void;
33
+ };
34
+
35
+ type OptionItemProps = {
36
+ isLast: boolean;
37
+ isSelected: boolean;
38
+ label: string;
39
+ onPress: () => void;
40
+ };
41
+
42
+ function OptionItem(props: OptionItemProps) {
43
+ const { isLast, isSelected, label, onPress } = props;
44
+
45
+ return (
46
+ <Pressable
47
+ style={[
48
+ tw`flex-row items-center justify-between px-4 py-3`,
49
+ !isLast && tw`dark:border-divider border-b border-gray-200`,
50
+ ]}
51
+ onPress={onPress}
52
+ >
53
+ <View style={tw`flex-1`}>
54
+ <Typography
55
+ style={[
56
+ tw`text-base`,
57
+ isSelected ? tw`text-primary-500 font-medium` : tw`dark:text-foreground text-gray-800`,
58
+ ]}
59
+ >
60
+ {label}
61
+ </Typography>
62
+ </View>
63
+ </Pressable>
64
+ );
65
+ }
66
+
67
+ export function SelectDropdown(props: Props) {
68
+ const { label, onSelect, options, placeholder = 'Select', selectedId, style } = props;
69
+ const [isOpen, setIsOpen] = useState<boolean>(false);
70
+ const [dropdownLayout, setDropdownLayout] = useState<DropdownLayout | null>(null);
71
+ const triggerRef = useRef<View>(null);
72
+
73
+ const selectedOption = useMemo(
74
+ () => options.find((option) => option.id === selectedId),
75
+ [options, selectedId],
76
+ );
77
+ const selectedLabel = selectedOption?.label ?? placeholder;
78
+ const lastIndex = options.length - 1;
79
+
80
+ const handleToggle = useCallback(() => {
81
+ setIsOpen((prev) => !prev);
82
+ }, []);
83
+
84
+ const handleClose = useCallback(() => {
85
+ setIsOpen(false);
86
+ }, []);
87
+
88
+ const handleSelect = useCallback(
89
+ (optionId: string) => {
90
+ onSelect(optionId);
91
+ setIsOpen(false);
92
+ },
93
+ [onSelect],
94
+ );
95
+
96
+ useEffect(() => {
97
+ if (!isOpen) {
98
+ return;
99
+ }
100
+
101
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
102
+ setDropdownLayout({ height, width, x, y });
103
+ });
104
+ }, [isOpen]);
105
+
106
+ const optionPressHandlers = useMemo(() => {
107
+ const handlers: Record<string, () => void> = {};
108
+
109
+ options.forEach((option) => {
110
+ handlers[option.id] = () => handleSelect(option.id);
111
+ });
112
+
113
+ return handlers;
114
+ }, [handleSelect, options]);
115
+
116
+ function renderOption(option: DropdownOption, index: number) {
117
+ const handlePress = optionPressHandlers[option.id];
118
+
119
+ return (
120
+ <OptionItem
121
+ key={option.id}
122
+ isLast={index === lastIndex}
123
+ isSelected={option.id === selectedId}
124
+ label={option.label}
125
+ onPress={handlePress}
126
+ />
127
+ );
128
+ }
129
+
130
+ function renderMenu() {
131
+ if (!dropdownLayout) {
132
+ return null;
133
+ }
134
+
135
+ const screenHeight = Dimensions.get('window').height;
136
+ const estimatedMenuHeight = Math.min(MAX_MENU_HEIGHT, options.length * OPTION_ROW_HEIGHT);
137
+ const spaceBelow = screenHeight - (dropdownLayout.y + dropdownLayout.height) - DROPDOWN_OFFSET;
138
+ const spaceAbove = dropdownLayout.y - DROPDOWN_OFFSET;
139
+ const canOpenBelow = spaceBelow >= estimatedMenuHeight;
140
+ const canOpenAbove = spaceAbove >= estimatedMenuHeight;
141
+ const shouldOpenBelow =
142
+ canOpenBelow || (!canOpenBelow && !canOpenAbove && spaceBelow >= spaceAbove);
143
+ const preferredTop = shouldOpenBelow
144
+ ? dropdownLayout.y + dropdownLayout.height + DROPDOWN_OFFSET
145
+ : dropdownLayout.y - estimatedMenuHeight - DROPDOWN_OFFSET;
146
+ const clampedTop = Math.min(
147
+ Math.max(0, preferredTop),
148
+ Math.max(0, screenHeight - estimatedMenuHeight - DROPDOWN_OFFSET),
149
+ );
150
+
151
+ return (
152
+ <View
153
+ style={[
154
+ tw`dark:border-divider dark:bg-sheet absolute z-50 max-h-80 rounded-xl border border-gray-200 bg-white`,
155
+ {
156
+ elevation: 5,
157
+ left: dropdownLayout.x,
158
+ shadowColor: colors.gray[900],
159
+ shadowOffset: {
160
+ height: 2,
161
+ width: 0,
162
+ },
163
+ shadowOpacity: 0.15,
164
+ shadowRadius: 4,
165
+ top: clampedTop,
166
+ width: dropdownLayout.width,
167
+ },
168
+ ]}
169
+ >
170
+ <ScrollView showsVerticalScrollIndicator={false} style={tw`max-h-80`}>
171
+ {options.map(renderOption)}
172
+ </ScrollView>
173
+ </View>
174
+ );
175
+ }
176
+
177
+ return (
178
+ <>
179
+ {label && (
180
+ <Typography style={tw`mb-1 text-sm font-medium text-gray-700 dark:text-gray-300`}>
181
+ {label}
182
+ </Typography>
183
+ )}
184
+ <Pressable
185
+ ref={triggerRef}
186
+ style={[
187
+ defaultInputContainerStyle,
188
+ tw`dark:border-divider dark:bg-surface items-center justify-between`,
189
+ style,
190
+ ]}
191
+ onPress={handleToggle}
192
+ >
193
+ <View style={tw`flex-1`}>
194
+ <Typography
195
+ style={[
196
+ defaultInputTextStyle,
197
+ tw`dark:text-foreground`,
198
+ !selectedOption && tw`dark:text-placeholder text-gray-400`,
199
+ ]}
200
+ >
201
+ {selectedLabel}
202
+ </Typography>
203
+ </View>
204
+ {isOpen ? (
205
+ <ChevronDownIcon height={20} style={tw`dark:text-subtitle text-gray-500`} width={20} />
206
+ ) : (
207
+ <ChevronRightIcon height={20} style={tw`dark:text-subtitle text-gray-500`} width={20} />
208
+ )}
209
+ </Pressable>
210
+
211
+ <Modal
212
+ containerStyle={tw`flex-1 p-0`}
213
+ isVisible={isOpen}
214
+ onBackButtonPress={handleClose}
215
+ onBackdropPress={handleClose}
216
+ >
217
+ <View pointerEvents="box-none" style={tw`flex-1`}>
218
+ {renderMenu()}
219
+ </View>
220
+ </Modal>
221
+ </>
222
+ );
223
+ }
@@ -36,7 +36,7 @@ export function Skeleton(props: Props) {
36
36
  }
37
37
 
38
38
  return (
39
- <Animated.View style={[tw`rounded-lg bg-gray-200`, animatedStyle, style]}>
39
+ <Animated.View style={[tw`dark:bg-surface rounded-lg bg-gray-200`, animatedStyle, style]}>
40
40
  <View style={tw`opacity-0`}>{children}</View>
41
41
  </Animated.View>
42
42
  );
@@ -88,6 +88,7 @@ export function BottomSheetTextInput(props: BottomSheetTextInputProps) {
88
88
  <View
89
89
  style={[
90
90
  defaultInputContainerStyle,
91
+ tw`dark:border-divider dark:bg-surface`,
91
92
  focusedInputStyle(isFocused),
92
93
  disabledInputStyle(isDisabled),
93
94
  style,
@@ -100,8 +101,8 @@ export function BottomSheetTextInput(props: BottomSheetTextInputProps) {
100
101
  multiline={multiline}
101
102
  placeholder={placeholder}
102
103
  placeholderTextColor={colors.gray[500]}
103
- selectionColor={colors.primary}
104
- style={[defaultInputTextStyle, textStyle]}
104
+ selectionColor={colors.primary[400]}
105
+ style={[defaultInputTextStyle, tw`dark:text-foreground`, textStyle]}
105
106
  value={value}
106
107
  onBlur={(e) => handleOnBlur(e as NativeSyntheticEvent<TextInputFocusEventData>)}
107
108
  onChangeText={handleOnChangeText}
@@ -115,7 +116,7 @@ export function BottomSheetTextInput(props: BottomSheetTextInputProps) {
115
116
  testID="clear-button"
116
117
  onPress={handleOnClearPress}
117
118
  >
118
- <CrossIcon />
119
+ <CrossIcon style={tw`text-subtitle`} />
119
120
  </Pressable>
120
121
  )}
121
122
  </View>
@@ -11,6 +11,7 @@ import {
11
11
  import { DefaultNameInputProps } from './constants';
12
12
 
13
13
  import { CrossIcon } from '@/icons';
14
+ import { useLocalStorageStore } from '@/stores';
14
15
  import {
15
16
  colors,
16
17
  defaultInputContainerStyle,
@@ -43,6 +44,7 @@ export function TextInput(props: TextInputProps) {
43
44
  value,
44
45
  ...extraProps
45
46
  } = props;
47
+ const colorScheme = useLocalStorageStore((s) => s.colorScheme);
46
48
  const [isClearButtonVisible, setIsClearButtonVisible] = useState<boolean>(false);
47
49
  const [isFocused, setFocused] = useState<boolean>(false);
48
50
 
@@ -81,6 +83,7 @@ export function TextInput(props: TextInputProps) {
81
83
  <View
82
84
  style={[
83
85
  defaultInputContainerStyle,
86
+ tw`dark:border-divider dark:bg-surface`,
84
87
  focusedInputStyle(isFocused),
85
88
  disabledInputStyle(isDisabled),
86
89
  style,
@@ -89,12 +92,13 @@ export function TextInput(props: TextInputProps) {
89
92
  <RNTextInput
90
93
  {...DefaultNameInputProps}
91
94
  ref={textInputRef}
95
+ cursorColor={colors.primary[400]}
92
96
  editable={!isDisabled}
93
97
  multiline={multiline}
94
98
  placeholder={placeholder}
95
- placeholderTextColor={colors.gray[500]}
96
- selectionColor={colors.primary}
97
- style={[defaultInputTextStyle, textStyle]}
99
+ placeholderTextColor={colorScheme === 'dark' ? colors.placeholder : colors.gray[400]}
100
+ selectionColor={colors.primary[400]}
101
+ style={[defaultInputTextStyle, tw`dark:text-foreground`, textStyle]}
98
102
  value={value}
99
103
  onBlur={handleOnBlur}
100
104
  onChangeText={handleOnChangeText}
@@ -108,7 +112,7 @@ export function TextInput(props: TextInputProps) {
108
112
  testID="clear-button"
109
113
  onPress={handleOnClearPress}
110
114
  >
111
- <CrossIcon style={tw`text-gray-400`} />
115
+ <CrossIcon style={tw`dark:text-subtitle text-gray-400`} />
112
116
  </Pressable>
113
117
  )}
114
118
  </View>
@@ -0,0 +1 @@
1
+ export * from './theme-manager.component';
@@ -0,0 +1,27 @@
1
+ import { useEffect } from 'react';
2
+ import { StatusBar } from 'react-native';
3
+ import { useDeviceContext } from 'twrnc';
4
+
5
+ import CONFIG from '@/config';
6
+ import { useLocalStorageStore } from '@/stores';
7
+ import { tw } from '@/tailwind';
8
+
9
+ export function ThemeManager() {
10
+ const storedScheme = useLocalStorageStore((s) => s.colorScheme);
11
+ useDeviceContext(tw, {
12
+ initialColorScheme: storedScheme,
13
+ observeDeviceColorSchemeChanges: false,
14
+ });
15
+
16
+ const isDark = storedScheme === 'dark';
17
+
18
+ useEffect(() => {
19
+ StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
20
+ if (CONFIG.IS_ANDROID) {
21
+ StatusBar.setBackgroundColor('transparent');
22
+ StatusBar.setTranslucent(true);
23
+ }
24
+ }, [isDark]);
25
+
26
+ return null;
27
+ }
@@ -0,0 +1,3 @@
1
+ export * from './toast-manager.component';
2
+ export * from './toast-manager.types';
3
+ export * from './toast.service';
@@ -0,0 +1,109 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+ import Animated, { SlideInDown, SlideOutDown } from 'react-native-reanimated';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+ import { SvgProps } from 'react-native-svg';
6
+
7
+ import { ToastManagerRef, ToastOptions, ToastVariant } from './toast-manager.types';
8
+ import { Toast } from './toast.service';
9
+
10
+ import { Typography } from '@/components/atoms/Typography';
11
+ import { AlertTriangleIcon, CheckCircleIcon, InfoIcon, XCircleIcon } from '@/icons';
12
+ import { colors, tw } from '@/tailwind';
13
+
14
+ type QueuedToast = ToastOptions & {
15
+ variant: ToastVariant;
16
+ };
17
+
18
+ type VariantConfig = {
19
+ color: string;
20
+ icon: React.FC<SvgProps>;
21
+ };
22
+
23
+ const ANIMATION_DURATION = 300;
24
+ const DEFAULT_DURATION = 3000;
25
+ const ICON_SIZE = 20;
26
+ const BOTTOM_PADDING_OFFSET = 16;
27
+
28
+ const VARIANT_CONFIG: Record<ToastVariant, VariantConfig> = {
29
+ error: { color: colors.error, icon: XCircleIcon },
30
+ info: { color: colors.secondary[500], icon: InfoIcon },
31
+ success: { color: colors.success, icon: CheckCircleIcon },
32
+ warning: { color: colors.primary[500], icon: AlertTriangleIcon },
33
+ };
34
+
35
+ export function ToastManager() {
36
+ const [queue, setQueue] = useState<QueuedToast[]>([]);
37
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
38
+ const insets = useSafeAreaInsets();
39
+
40
+ const currentToast = queue[0];
41
+
42
+ const handleDismiss = useCallback(() => {
43
+ if (timerRef.current) {
44
+ clearTimeout(timerRef.current);
45
+ timerRef.current = null;
46
+ }
47
+ setQueue((prev) => prev.slice(1));
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ const ref: ToastManagerRef = {
52
+ show: (variant: ToastVariant, options: ToastOptions) => {
53
+ setQueue((prev) => [...prev, { ...options, variant }]);
54
+ },
55
+ };
56
+ Toast.setRef(ref);
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ if (!currentToast) {
61
+ return;
62
+ }
63
+
64
+ const duration = currentToast.duration ?? DEFAULT_DURATION;
65
+ timerRef.current = setTimeout(() => {
66
+ setQueue((prev) => prev.slice(1));
67
+ }, duration);
68
+
69
+ return () => {
70
+ if (timerRef.current) {
71
+ clearTimeout(timerRef.current);
72
+ timerRef.current = null;
73
+ }
74
+ };
75
+ }, [currentToast]);
76
+
77
+ if (!currentToast) {
78
+ return null;
79
+ }
80
+
81
+ const config = VARIANT_CONFIG[currentToast.variant];
82
+ const IconComponent = config.icon;
83
+
84
+ return (
85
+ <View pointerEvents="box-none" style={tw`absolute bottom-10 left-6 right-6 z-50 items-center`}>
86
+ <Animated.View
87
+ key={`${currentToast.variant}-${currentToast.message}`}
88
+ entering={SlideInDown.duration(ANIMATION_DURATION)}
89
+ exiting={SlideOutDown.duration(ANIMATION_DURATION)}
90
+ style={[tw`mx-4 w-full max-w-sm`, { paddingBottom: insets.bottom + BOTTOM_PADDING_OFFSET }]}
91
+ >
92
+ <Pressable
93
+ style={tw`dark:border-divider dark:bg-sheet flex-row items-center rounded-xl border border-gray-200 bg-white px-4 py-3.5 shadow-sm`}
94
+ onPress={handleDismiss}
95
+ >
96
+ <View style={tw`mr-3`}>
97
+ <IconComponent color={config.color} height={ICON_SIZE} width={ICON_SIZE} />
98
+ </View>
99
+ <Typography
100
+ numberOfLines={2}
101
+ style={tw`dark:text-foreground flex-1 text-sm font-medium text-gray-800`}
102
+ >
103
+ {currentToast.message}
104
+ </Typography>
105
+ </Pressable>
106
+ </Animated.View>
107
+ </View>
108
+ );
109
+ }
@@ -0,0 +1,10 @@
1
+ export type ToastVariant = 'error' | 'info' | 'success' | 'warning';
2
+
3
+ export type ToastOptions = {
4
+ duration?: number;
5
+ message: string;
6
+ };
7
+
8
+ export type ToastManagerRef = {
9
+ show: (variant: ToastVariant, options: ToastOptions) => void;
10
+ };
@@ -0,0 +1,27 @@
1
+ import { ToastManagerRef, ToastOptions } from './toast-manager.types';
2
+
3
+ class ToastService {
4
+ private static ref: ToastManagerRef | null = null;
5
+
6
+ static error(message: string, options?: Omit<ToastOptions, 'message'>) {
7
+ ToastService.ref?.show('error', { message, ...options });
8
+ }
9
+
10
+ static info(message: string, options?: Omit<ToastOptions, 'message'>) {
11
+ ToastService.ref?.show('info', { message, ...options });
12
+ }
13
+
14
+ static setRef(ref: ToastManagerRef) {
15
+ ToastService.ref = ref;
16
+ }
17
+
18
+ static success(message: string, options?: Omit<ToastOptions, 'message'>) {
19
+ ToastService.ref?.show('success', { message, ...options });
20
+ }
21
+
22
+ static warning(message: string, options?: Omit<ToastOptions, 'message'>) {
23
+ ToastService.ref?.show('warning', { message, ...options });
24
+ }
25
+ }
26
+
27
+ export { ToastService as Toast };
@@ -16,7 +16,7 @@ export function Typography(props: Props): JSX.Element {
16
16
  return (
17
17
  <RNText
18
18
  {...shouldTruncateTextProps}
19
- style={[tw`font-sans text-base font-normal text-black`, style]}
19
+ style={[tw`dark:text-foreground font-sans text-base font-normal text-gray-900`, style]}
20
20
  {...extraProps}
21
21
  />
22
22
  );
@@ -1,13 +1,21 @@
1
+ export * from './AlertManager';
1
2
  export * from './BottomSheet';
2
3
  export * from './Button';
4
+ export * from './DateModalInput';
5
+ export * from './DatePicker';
6
+ export * from './DateTextInput';
3
7
  export * from './Divider';
4
8
  export * from './ExcludedEdges';
9
+ export * from './GradientBackground';
5
10
  export * from './InputLayout';
6
11
  export * from './KeyboardAccessory';
7
12
  export * from './KeyboardAwareScrollView';
8
13
  export * from './ListLoadingItem';
9
14
  export * from './Modal';
10
15
  export * from './ScreenLoader';
16
+ export * from './SelectDropdown';
11
17
  export * from './Skeleton';
12
18
  export * from './TextInput';
19
+ export * from './ThemeManager';
20
+ export * from './ToastManager';
13
21
  export * from './Typography';
@@ -51,7 +51,7 @@ export function BackButton(props: Props) {
51
51
  onPress={handleOnPress}
52
52
  >
53
53
  <View style={[tw`flex-1 items-center justify-center rounded-full`, style]}>
54
- <ArrowLeftIcon style={tw`text-gray-950`} />
54
+ <ArrowLeftIcon style={tw`dark:text-foreground text-gray-900`} />
55
55
  </View>
56
56
  </TouchableOpacity>
57
57
  );