nx-react-native-cli 2.7.1 → 3.0.1
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/lib/index.cjs +27 -27
- package/package.json +1 -1
- package/templates/shared/apps/mobile/src/app/index.tsx +13 -8
- package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.component.tsx +134 -0
- package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.types.ts +18 -0
- package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert.service.ts +27 -0
- package/templates/shared/apps/mobile/src/components/atoms/AlertManager/index.ts +3 -0
- package/templates/shared/apps/mobile/src/components/atoms/BottomSheet/bottom-sheet.component.tsx +14 -8
- package/templates/shared/apps/mobile/src/components/atoms/Button/button.component.tsx +1 -1
- package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/date-modal-input.component.tsx +69 -0
- package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/index.ts +1 -0
- package/templates/shared/apps/mobile/src/components/atoms/DatePicker/date-picker.component.tsx +44 -0
- package/templates/shared/apps/mobile/src/components/atoms/DatePicker/index.ts +1 -0
- package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/date-text-input.component.tsx +218 -0
- package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/index.ts +1 -0
- package/templates/shared/apps/mobile/src/components/atoms/Divider/divider-component.tsx +1 -1
- package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/gradient-background.component.tsx +45 -0
- package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/index.ts +1 -0
- package/templates/shared/apps/mobile/src/components/atoms/InputLayout/input-layout.component.tsx +12 -4
- package/templates/shared/apps/mobile/src/components/atoms/KeyboardAccessory/keyboard-accessory.component.tsx +6 -3
- package/templates/shared/apps/mobile/src/components/atoms/Modal/modal.component.tsx +2 -0
- package/templates/shared/apps/mobile/src/components/atoms/ScreenLoader/screen-loader.component.tsx +6 -1
- package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/index.ts +1 -0
- package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/select-dropdown.component.tsx +223 -0
- package/templates/shared/apps/mobile/src/components/atoms/Skeleton/skeleton.component.tsx +1 -1
- package/templates/shared/apps/mobile/src/components/atoms/TextInput/bottom-sheet-text-input.component.tsx +4 -3
- package/templates/shared/apps/mobile/src/components/atoms/TextInput/text-input.component.tsx +8 -4
- package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/index.ts +1 -0
- package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/theme-manager.component.tsx +27 -0
- package/templates/shared/apps/mobile/src/components/atoms/ToastManager/index.ts +3 -0
- package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.component.tsx +109 -0
- package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.types.ts +10 -0
- package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast.service.ts +27 -0
- package/templates/shared/apps/mobile/src/components/atoms/Typography/typography.component.tsx +1 -1
- package/templates/shared/apps/mobile/src/components/atoms/index.ts +8 -0
- package/templates/shared/apps/mobile/src/components/molecules/BackButton/back-button.component.tsx +1 -1
- package/templates/shared/apps/mobile/src/components/molecules/ScreenContainer/screen-container.component.tsx +2 -22
- package/templates/shared/apps/mobile/src/components/molecules/ScreenHeader/screen-header.component.tsx +2 -2
- package/templates/shared/apps/mobile/src/hooks/index.ts +1 -0
- package/templates/shared/apps/mobile/src/hooks/usePushNotifications.hook.ts +104 -0
- package/templates/shared/apps/mobile/src/hooks/useToggleDarkMode.hook.tsx +24 -2
- package/templates/shared/apps/mobile/src/icons/alert-triangle.svg +5 -0
- package/templates/shared/apps/mobile/src/icons/check-circle.svg +4 -0
- package/templates/shared/apps/mobile/src/icons/chevron-down.svg +1 -0
- package/templates/shared/apps/mobile/src/icons/chevron-right.svg +1 -0
- package/templates/shared/apps/mobile/src/icons/index.ts +18 -1
- package/templates/shared/apps/mobile/src/icons/info.svg +5 -0
- package/templates/shared/apps/mobile/src/icons/x-circle.svg +5 -0
- package/templates/shared/apps/mobile/src/routes/index.tsx +19 -14
- package/templates/shared/apps/mobile/src/screens/LandingScreen/landing.screen.tsx +232 -8
- package/templates/shared/apps/mobile/src/stores/local-storage.store.ts +9 -5
- package/templates/shared/apps/mobile/src/stores/theme.slice.ts +15 -0
- package/templates/shared/apps/mobile/src/stores/user.slice.ts +5 -1
- package/templates/shared/apps/mobile/src/tailwind/index.ts +3 -3
- package/templates/shared/apps/mobile/tailwind.config.js +14 -0
- 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';
|
package/templates/shared/apps/mobile/src/components/atoms/InputLayout/input-layout.component.tsx
CHANGED
|
@@ -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-
|
|
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]
|
|
19
|
-
<Button
|
|
20
|
-
|
|
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>
|
package/templates/shared/apps/mobile/src/components/atoms/ScreenLoader/screen-loader.component.tsx
CHANGED
|
@@ -10,7 +10,12 @@ export function ScreenLoader(props: Props) {
|
|
|
10
10
|
const { style } = props;
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
|
-
<View
|
|
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>
|
package/templates/shared/apps/mobile/src/components/atoms/TextInput/text-input.component.tsx
CHANGED
|
@@ -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[
|
|
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';
|
package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/theme-manager.component.tsx
ADDED
|
@@ -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
|
+
}
|
package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.component.tsx
ADDED
|
@@ -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,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 };
|
package/templates/shared/apps/mobile/src/components/atoms/Typography/typography.component.tsx
CHANGED
|
@@ -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-
|
|
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';
|
package/templates/shared/apps/mobile/src/components/molecules/BackButton/back-button.component.tsx
CHANGED
|
@@ -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-
|
|
54
|
+
<ArrowLeftIcon style={tw`dark:text-foreground text-gray-900`} />
|
|
55
55
|
</View>
|
|
56
56
|
</TouchableOpacity>
|
|
57
57
|
);
|