ota-components-module 1.3.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 (36) hide show
  1. package/README.md +179 -0
  2. package/assets/images/ic_camera.svg +3 -0
  3. package/assets/images/ic_close.svg +8 -0
  4. package/assets/images/ic_folder.svg +3 -0
  5. package/assets/images/placeholder.png +0 -0
  6. package/expo-env.d.ts +7 -0
  7. package/mri-manifest.json +10 -0
  8. package/package.json +28 -0
  9. package/src/button/ThemedButton.tsx +120 -0
  10. package/src/feedback/ActivityLoader.tsx +84 -0
  11. package/src/feedback/CustomAlert.tsx +143 -0
  12. package/src/feedback/DeleteImageConfirmationDialog.tsx +58 -0
  13. package/src/feedback/ProgressBar.tsx +58 -0
  14. package/src/image/ImagePickerBottomSheet.tsx +61 -0
  15. package/src/image/ImagePickerView.tsx +103 -0
  16. package/src/image/MultipleImagePreview.tsx +424 -0
  17. package/src/image/StackedImage.tsx +155 -0
  18. package/src/index.ts +68 -0
  19. package/src/input/CustomDropdown.tsx +142 -0
  20. package/src/input/CustomInput.tsx +101 -0
  21. package/src/input/FormField.tsx +358 -0
  22. package/src/input/KeyboardScrollView.tsx +131 -0
  23. package/src/input/SearchViewInput.tsx +183 -0
  24. package/src/layout/BottomSheetDialog.tsx +208 -0
  25. package/src/layout/BottomTwoButtonLayoutComponent.tsx +153 -0
  26. package/src/layout/CardView.tsx +101 -0
  27. package/src/layout/PropertyHeaderComponent.tsx +110 -0
  28. package/src/list/SearchableList.tsx +273 -0
  29. package/src/models/PropertyImage.ts +20 -0
  30. package/src/typography/Label.tsx +225 -0
  31. package/src/utils/BaseStyle.ts +46 -0
  32. package/src/utils/Strings.ts +1 -0
  33. package/src/utils/TextConstants.ts +24 -0
  34. package/src/utils/Utils.ts +11 -0
  35. package/src/webbaseview/WebBaseView.tsx +26 -0
  36. package/tsconfig.json +9 -0
@@ -0,0 +1,273 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ View,
4
+ FlatList,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import { Image } from 'expo-image';
10
+ import { Colors } from '../utils/BaseStyle';
11
+ import Label from '../typography/Label';
12
+ import ThemedButton from '../button/ThemedButton';
13
+ import { TextSize, TextWeight } from '../utils/TextConstants';
14
+ import { Ionicons } from '@expo/vector-icons';
15
+ import SearchViewInput from '../input/SearchViewInput';
16
+
17
+ // Define the item structure
18
+ export interface ListItem {
19
+ id: string;
20
+ title: string;
21
+ imageUri?: string;
22
+ }
23
+
24
+ interface SearchableListProps {
25
+ /**
26
+ * Test ID for the component
27
+ */
28
+ testID: string;
29
+
30
+ /**
31
+ * Array of items to display in the list
32
+ */
33
+ items: ListItem[];
34
+
35
+ /**
36
+ * Callback when an item is selected
37
+ */
38
+ onItemSelect: (item: ListItem) => void;
39
+
40
+ /**
41
+ * Callback when the confirm button is pressed
42
+ */
43
+ onConfirm: (selectedItem: ListItem | null) => void;
44
+
45
+ /**
46
+ * Title to display at the top of the list
47
+ */
48
+ title?: string;
49
+
50
+ /**
51
+ * Custom style for the container
52
+ */
53
+ containerStyle?: ViewStyle;
54
+
55
+ /**
56
+ * Placeholder text for the search input
57
+ */
58
+ searchPlaceholder?: string;
59
+
60
+ /**
61
+ * Text for the confirm button
62
+ */
63
+ confirmButtonText?: string;
64
+
65
+ /**
66
+ * Callback when the close button is pressed
67
+ */
68
+ onClose?: () => void;
69
+ }
70
+
71
+ /**
72
+ * A searchable list component with selection and confirmation
73
+ */
74
+ const SearchableList: React.FC<SearchableListProps> = ({
75
+ testID,
76
+ items,
77
+ onItemSelect,
78
+ onConfirm,
79
+ title = 'Select item',
80
+ containerStyle,
81
+ searchPlaceholder = 'Search',
82
+ confirmButtonText = 'Confirm',
83
+ onClose
84
+ }) => {
85
+ const [searchQuery, setSearchQuery] = useState('');
86
+ const [filteredItems, setFilteredItems] = useState<ListItem[]>(items);
87
+ const [selectedItem, setSelectedItem] = useState<ListItem | null>(null);
88
+
89
+ // Filter items based on search query
90
+ useEffect(() => {
91
+ if (searchQuery.trim() === '') {
92
+ setFilteredItems(items);
93
+ } else {
94
+ const filtered = items.filter(item =>
95
+ item.title.toLowerCase().includes(searchQuery.toLowerCase())
96
+ );
97
+ setFilteredItems(filtered);
98
+ }
99
+ }, [searchQuery, items]);
100
+
101
+ // Handle item selection
102
+ const handleItemPress = (item: ListItem) => {
103
+ setSelectedItem(item);
104
+ onItemSelect(item);
105
+ };
106
+
107
+ // Handle confirm button press
108
+ const handleConfirm = () => {
109
+ onConfirm(selectedItem);
110
+ };
111
+
112
+ // Render each list item
113
+ const renderItem = ({ item }: { item: ListItem }) => {
114
+ const isSelected = selectedItem?.id === item.id;
115
+
116
+ return (
117
+ <TouchableOpacity
118
+ testID={`propertyListItem${item.id}`}
119
+ style={[
120
+ styles.itemContainer,
121
+ isSelected && styles.selectedItemContainer,
122
+ ]}
123
+ onPress={() => handleItemPress(item)}
124
+ activeOpacity={0.7}
125
+ >
126
+ {item.imageUri && (<Image
127
+ key={item.id}
128
+ source={item.imageUri}
129
+ contentFit="cover"
130
+ placeholderContentFit='cover'
131
+ transition={500}
132
+ cachePolicy="disk"
133
+ placeholder={require("../../assets/images/placeholder.png")}
134
+ style={styles.itemImage}
135
+ />)}
136
+ <Label
137
+ text={item.title}
138
+ size={TextSize.NORMAL}
139
+ weight={TextWeight.NORMAL}
140
+ textColorType={isSelected ? "primary" : "primary"}
141
+ customStyle={isSelected ? styles.selectedItemText : styles.itemText}
142
+ />
143
+ </TouchableOpacity>
144
+ );
145
+ };
146
+
147
+ return (
148
+ <View style={[styles.container, containerStyle]}>
149
+ {/* Title and Close Button */}
150
+ {title && (
151
+ <View style={styles.header}>
152
+ <Label
153
+ text={title}
154
+ size={TextSize.LARGE}
155
+ weight={TextWeight.BOLD}
156
+ textColorType="primary"
157
+ customStyle={styles.title}
158
+ />
159
+
160
+ {onClose && (
161
+ <TouchableOpacity testID='btnCloseSearchableList' onPress={onClose}>
162
+ <Ionicons name="close" size={24} color={Colors.primaryTextColor} />
163
+ </TouchableOpacity>
164
+ )}
165
+ </View>
166
+ )}
167
+
168
+ {/* Search Input */}
169
+ <SearchViewInput
170
+ testID="txtSearchProperty"
171
+ value={searchQuery}
172
+ onChangeText={setSearchQuery}
173
+ placeholder={searchPlaceholder}
174
+ containerStyle={styles.searchContainer}
175
+ onClear={() => setSearchQuery('')}
176
+ />
177
+
178
+ {/* List of Items */}
179
+ <FlatList
180
+ testID={testID}
181
+ data={filteredItems}
182
+ renderItem={renderItem}
183
+ keyExtractor={(item) => item.id}
184
+ style={styles.list}
185
+ showsVerticalScrollIndicator={false}
186
+ />
187
+
188
+ {/* Confirm Button */}
189
+ <View style={[styles.buttonContainer, !selectedItem && styles.confirmButtonDisabled]}>
190
+ <ThemedButton
191
+ testID='btnConfirmSearchableList'
192
+ title={confirmButtonText}
193
+ onPress={handleConfirm}
194
+ disabled={!selectedItem}
195
+ type="secondary"
196
+ size="regular"
197
+ style={styles.confirmButton}
198
+ />
199
+ </View>
200
+ </View>
201
+ );
202
+ };
203
+
204
+ const styles = StyleSheet.create({
205
+ container: {
206
+ flex: 1,
207
+ width: '100%',
208
+ },
209
+ header: {
210
+ flexDirection: 'row',
211
+ justifyContent: 'space-between',
212
+ alignItems: 'center',
213
+ marginBottom: 30,
214
+ paddingHorizontal: 8,
215
+ },
216
+ title: {
217
+ marginBottom: 0,
218
+ },
219
+ searchContainer: {
220
+ marginBottom: 16,
221
+ marginHorizontal: 4
222
+ },
223
+ list: {
224
+ flex: 1,
225
+ marginBottom: 16
226
+ },
227
+ itemContainer: {
228
+ flexDirection: 'row',
229
+ alignItems: 'center',
230
+ paddingVertical: 16,
231
+ paddingHorizontal: 6,
232
+ borderRadius: 4,
233
+ marginBottom: 4,
234
+ },
235
+ selectedItemContainer: {
236
+ backgroundColor: Colors.lightThemePrimaryColor,
237
+ },
238
+ itemImage: {
239
+ width: 40,
240
+ height: 40,
241
+ borderRadius: 10,
242
+ marginRight: 12,
243
+ },
244
+ imagePlaceholder: {
245
+ width: 40,
246
+ height: 40,
247
+ borderRadius: 4,
248
+ backgroundColor: Colors.darkGrayColor,
249
+ marginRight: 12,
250
+ },
251
+ itemText: {
252
+ color: Colors.primaryTextColor,
253
+ },
254
+ selectedItemText: {
255
+ color: Colors.whiteColor,
256
+ },
257
+ buttonContainer: {
258
+ width: '100%',
259
+ paddingVertical: 16,
260
+ marginBottom: 16,
261
+ paddingHorizontal: 8
262
+ },
263
+ confirmButton: {
264
+ width: '100%',
265
+ height: 56,
266
+ },
267
+ confirmButtonDisabled: {
268
+ width: '100%',
269
+ opacity: 0.5,
270
+ }
271
+ });
272
+
273
+ export default SearchableList;
@@ -0,0 +1,20 @@
1
+ export class PropertyImage {
2
+ propertyId: number | undefined = 0;
3
+ propertyImagesId: number | null = null;
4
+ imageBlobUrl = "";
5
+ imageType: string | undefined = "";
6
+ imageName: string | undefined = "";
7
+ isDefault = false;
8
+ id?: string;
9
+ isLocal?: boolean;
10
+
11
+ constructor(data: any) {
12
+ this.propertyId = data?.propertyId || 0;
13
+ this.propertyImagesId = data?.propertyImagesId || null;
14
+ this.imageBlobUrl = data?.imageBlobUrl || "";
15
+ this.imageType = data?.imageType || "";
16
+ this.imageName = data?.imageName || "";
17
+ this.isDefault = data?.isDefault || false;
18
+ this.id = data?.id || "";
19
+ }
20
+ }
@@ -0,0 +1,225 @@
1
+ import React from 'react';
2
+ import { Text, StyleSheet, TextStyle, StyleProp, useWindowDimensions } from 'react-native';
3
+ import { Colors } from '../utils/BaseStyle';
4
+ import { isWebDevice, MAX_RESOLUTION_MOBILE } from '../utils/Utils';
5
+
6
+ export type FontWeight = 'thin' | 'light' | 'normal' | 'bold' | 'extra-bold' | 'semi-light' | 'semi-regular' | 'semi-bold';
7
+ export type FontStyle = 'normal' | 'italic';
8
+ export type TextAlign = 'auto' | 'left' | 'right' | 'center' | 'justify';
9
+ type TextColorType = 'primary' | 'secondary';
10
+ export type LabelType = 'link' | 'label';
11
+
12
+ export type FontSize =
13
+ | 'tiny' // 12
14
+ | 'small' // 14
15
+ | 'normal' // 16
16
+ | 'large' // 18
17
+ | 'xlarge' //20
18
+ | 'xxlarge' //22
19
+ | 'xxxlarge' //24
20
+ | 'xxxxlarge' //26
21
+ | 'xxxxxlarge' //28
22
+ | 'xxxxxxlarge' //30
23
+ | 'header'; // 32
24
+
25
+ export type LabelVariant =
26
+ | 'heading1'
27
+ | 'heading2'
28
+ | 'heading3'
29
+ | 'title'
30
+ | 'subtitle'
31
+ | 'body'
32
+ | 'bodyBold'
33
+ | 'caption'
34
+ | 'captionBold'
35
+ | 'label'
36
+ | 'value'
37
+ | 'email'
38
+ | 'address';
39
+
40
+ interface LabelProps {
41
+ testID?: string;
42
+ labelType?: LabelType
43
+ text: string;
44
+ variant?: LabelVariant;
45
+ size?: FontSize;
46
+ weight?: FontWeight;
47
+ style?: FontStyle;
48
+ color?: string;
49
+ align?: TextAlign;
50
+ numberOfLines?: number;
51
+ customStyle?: StyleProp<TextStyle>;
52
+ onPress?: () => void;
53
+ textColorType?: TextColorType;
54
+ }
55
+
56
+ const Label: React.FC<LabelProps> = ({
57
+ testID,
58
+ labelType = 'label',
59
+ text,
60
+ variant = 'body',
61
+ size,
62
+ weight,
63
+ style: fontStyle,
64
+ color,
65
+ align,
66
+ numberOfLines,
67
+ customStyle,
68
+ onPress,
69
+ textColorType = 'primary',
70
+ }) => {
71
+
72
+ // Get base style from variant
73
+ const variantStyle = styles[variant] || styles.body;
74
+ const {width} = useWindowDimensions()
75
+ const isBigScreen = width >= MAX_RESOLUTION_MOBILE;
76
+
77
+ // Override with specific props if provided
78
+ const textStyle: TextStyle = {
79
+ ...variantStyle,
80
+ ...(size && { fontSize: getFontSize(size, isBigScreen) }),
81
+ ...(weight && { fontFamily: getFontFamily(weight) }),
82
+ ...(weight && { fontWeight: getFontWeight(weight) }),
83
+ ...(fontStyle && { fontStyle }),
84
+ ...(textColorType === 'primary' ? { color: Colors.primaryTextColor } : { color: Colors.secondaryTextColor }),
85
+ ...(color && { color }),
86
+ ...(align && { textAlign: align }),
87
+ };
88
+
89
+ return (
90
+ <Text
91
+ testID={testID}
92
+ style={[textStyle, customStyle, labelType === 'link' && styles.link]}
93
+ numberOfLines={numberOfLines}
94
+ onPress={onPress}
95
+ >
96
+ {text}
97
+ </Text>
98
+ );
99
+ };
100
+
101
+ // Helper function to get font size from size prop
102
+ const getFontSize = (size: FontSize, isBigScreen: boolean): number => {
103
+ const fontSizes = {
104
+ tiny: isBigScreen ? 14 : 12,
105
+ small: isBigScreen ? 16 : 14,
106
+ normal: isBigScreen ? 18 : 16,
107
+ large: isBigScreen ? 20 : 18,
108
+ xlarge: isBigScreen ? 22 : 20,
109
+ xxlarge: isBigScreen ? 24 : 22,
110
+ xxxlarge: isBigScreen ? 26 : 24,
111
+ xxxxlarge: isBigScreen ? 28 : 26,
112
+ xxxxxlarge: isBigScreen ? 30 : 28,
113
+ xxxxxxlarge: isBigScreen ? 32 : 30,
114
+ header: isBigScreen ? 34 : 32
115
+ };
116
+
117
+ return fontSizes[size];
118
+ };
119
+
120
+ // Helper function to map weight to fontWeight value
121
+ const getFontWeight = (weight: FontWeight): TextStyle['fontWeight'] => {
122
+ const weightMap = {
123
+ 'thin': '200',
124
+ 'light': '300',
125
+ 'semi-light': '400',
126
+ 'semi-regular': '500',
127
+ 'normal': 'normal',
128
+ 'semi-bold': '600',
129
+ 'bold': 'bold',
130
+ 'extra-bold': '800',
131
+ };
132
+
133
+ return weightMap[weight] as TextStyle['fontWeight'];
134
+ };
135
+
136
+ // Helper function to map weight to fontWeight value
137
+ const getFontFamily = (weight: FontWeight): TextStyle['fontFamily'] => {
138
+ const weightMap = {
139
+ 'thin': 'OpenSans-Light',
140
+ 'light': 'OpenSans-Light',
141
+ 'semi-light': 'OpenSans-Light',
142
+ 'semi-regular': 'OpenSans-Regular',
143
+ 'normal': 'OpenSans-Regular',
144
+ 'semi-bold': 'OpenSans-SemiBold',
145
+ 'bold': 'OpenSans-Bold',
146
+ 'extra-bold': 'OpenSans-Bold',
147
+ };
148
+
149
+ return weightMap[weight] as TextStyle['fontFamily'];
150
+ };
151
+
152
+ // Define styles for each variant
153
+ const styles = StyleSheet.create({
154
+ heading1: {
155
+ fontSize: 20, // xlarge
156
+ fontWeight: getFontWeight('extra-bold'),
157
+ color: '#1A1A1A',
158
+ },
159
+ heading2: {
160
+ fontSize: 18, // large
161
+ fontWeight: getFontWeight('bold'),
162
+ color: '#1A1A1A',
163
+ },
164
+ heading3: {
165
+ fontSize: 16, // normal
166
+ fontWeight: getFontWeight('bold'),
167
+ color: '#1A1A1A',
168
+ },
169
+ title: {
170
+ fontSize: 18, // large
171
+ fontWeight: getFontWeight('bold'),
172
+ color: '#1A1A1A',
173
+ },
174
+ subtitle: {
175
+ fontSize: 16, // normal
176
+ fontWeight: getFontWeight('bold'),
177
+ color: '#1A1A1A',
178
+ },
179
+ body: {
180
+ fontSize: 16, // normal
181
+ fontWeight: getFontWeight('normal'),
182
+ color: '#333333',
183
+ },
184
+ bodyBold: {
185
+ fontSize: 16, // normal
186
+ fontWeight: getFontWeight('bold'),
187
+ color: '#333333',
188
+ },
189
+ caption: {
190
+ fontSize: 14, // small
191
+ fontWeight: getFontWeight('normal'),
192
+ color: '#666666',
193
+ },
194
+ captionBold: {
195
+ fontSize: 14, // small
196
+ fontWeight: getFontWeight('bold'),
197
+ color: '#666666',
198
+ },
199
+ label: {
200
+ fontSize: 16, // normal
201
+ fontWeight: getFontWeight('light'),
202
+ color: '#666666',
203
+ },
204
+ value: {
205
+ fontSize: 18, // large
206
+ fontWeight: getFontWeight('bold'),
207
+ color: '#1A1A1A',
208
+ },
209
+ email: {
210
+ fontSize: 14, // small
211
+ fontWeight: getFontWeight('normal'),
212
+ color: '#666666',
213
+ },
214
+ address: {
215
+ fontSize: 14, // small
216
+ fontWeight: getFontWeight('normal'),
217
+ color: '#666666',
218
+ },
219
+ link: {
220
+ color: '#1e90ff',
221
+ textDecorationLine: 'underline',
222
+ },
223
+ });
224
+
225
+ export default Label;
@@ -0,0 +1,46 @@
1
+ export const Colors = {
2
+ // Primary colors
3
+ primaryColor: '#007AC6',
4
+ lightThemePrimaryColor: '#007AC6',
5
+ darkThemePrimaryColor: '#007AC6',
6
+
7
+ // Text colors
8
+ primaryTextColor: '#162029',
9
+ secondaryTextColor: '#607184',
10
+ textBlack: '#000000',
11
+ textWhite: '#FFFFFF',
12
+
13
+ // Background colors
14
+ backgroundColor: '#FFFFFF',
15
+ lightGrayColor: '#F5F5F5',
16
+ mediumGrayColor: '#E0E0E0',
17
+ darkGrayColor: '#9E9E9E',
18
+
19
+ // Status colors
20
+ successColor: '#4CAF50',
21
+ warningColor: '#FFC107',
22
+ errorColor: '#F44336',
23
+ infoColor: '#2196F3',
24
+
25
+ // Other UI colors
26
+ borderColor: '#E0E0E0',
27
+ shadowColor: '#000000',
28
+ disabledColor: '#BDBDBD',
29
+ bottomDeviderColor:'#ddd',
30
+
31
+ // Specific UI elements
32
+ buttonPrimaryColor: '#007AC6',
33
+ buttonSecondaryColor: '#FFFFFF',
34
+ inputBorderColor: '#E0E0E0',
35
+ inputFocusBorderColor: '#007AC6',
36
+
37
+ // Transparency colors
38
+ transparentBlack: 'rgba(0, 0, 0, 0.5)',
39
+ transparentWhite: 'rgba(255, 255, 255, 0.5)',
40
+
41
+ // Standard colors
42
+ whiteColor: '#FFFFFF',
43
+ blackColor: '#000000',
44
+ placeholderColor: '#607184',
45
+ textMidnightColor: '#162029',
46
+ };
@@ -0,0 +1 @@
1
+ export const delete_image_confirmation_msg = "Are you sure you wish to delete this image?";
@@ -0,0 +1,24 @@
1
+ export enum TextSize {
2
+ TINY = 'tiny', // 12
3
+ SMALL = 'small', // 14
4
+ NORMAL = 'normal', // 16
5
+ LARGE = 'large', // 18
6
+ XLARGE = 'xlarge', // 20
7
+ XXLARGE = 'xxlarge', // 22
8
+ XXXLARGE = 'xxxlarge', // 24
9
+ XXXXLARGE = 'xxxxlarge', // 26
10
+ XXXXXLARGE = 'xxxxxlarge', // 28
11
+ XXXXXXLARGE = 'xxxxxxlarge', // 30
12
+ HEADER = 'header', // 32
13
+ }
14
+
15
+ export enum TextWeight {
16
+ THIN = 'thin',
17
+ LIGHT = 'light',
18
+ NORMAL = 'normal',
19
+ SEMI_LIGHT = 'semi-light',
20
+ SEMI_REGULAR = 'semi-regular',
21
+ SEMI_BOLD = 'semi-bold',
22
+ BOLD = 'bold',
23
+ EXTRA_BOLD = 'extra-bold',
24
+ }
@@ -0,0 +1,11 @@
1
+ import { Platform } from "react-native";
2
+
3
+ export const isIOS = Platform.OS === 'ios';
4
+ export const isAndroid15AndAbove = Platform.OS === 'android' && parseInt(Platform.Version.toString(), 10) >= 35;
5
+
6
+ export const MAX_RESOLUTION_WEB = 1100;
7
+ export const MAX_RESOLUTION_MOBILE = 768;
8
+
9
+ export const isWebDevice = (width: number) => {
10
+ return width >= MAX_RESOLUTION_WEB ? true : false;
11
+ }
@@ -0,0 +1,26 @@
1
+ import { View, Text, StyleSheet, useWindowDimensions, ViewStyle } from 'react-native'
2
+ import React from 'react'
3
+ import { isWebDevice } from '../utils/Utils';
4
+
5
+ interface WebBaseViewProps {
6
+ children?: React.ReactNode;
7
+ viewStyle?: ViewStyle
8
+ }
9
+
10
+ const WebBaseView = ({ children, viewStyle }: WebBaseViewProps) => {
11
+ const { width } = useWindowDimensions();
12
+ const style = styles(width);
13
+ return (
14
+ <View style={[style.container, viewStyle]}>
15
+ {children}
16
+ </View>
17
+ )
18
+ }
19
+
20
+ const styles = (width: number) => StyleSheet.create({
21
+ container: {
22
+ ...(isWebDevice(width) ? {width: width * 0.8, marginHorizontal: 'auto'} : {width : width})
23
+ }
24
+ })
25
+
26
+ export default WebBaseView
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./lib",
5
+ "declaration": true
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["node_modules", "**/__tests__/*"]
9
+ }