-
38.gitignore
-
50README.md
-
41app.json
-
45app/(tabs)/_layout.tsx
-
109app/(tabs)/explore.tsx
-
74app/(tabs)/index.tsx
-
32app/+not-found.tsx
-
39app/_layout.tsx
-
BINassets/fonts/SpaceMono-Regular.ttf
-
BINassets/images/adaptive-icon.png
-
BINassets/images/favicon.png
-
BINassets/images/icon.png
-
BINassets/images/partial-react-logo.png
-
BINassets/images/react-logo.png
-
BINassets/images/react-logo@2x.png
-
BINassets/images/react-logo@3x.png
-
BINassets/images/splash-icon.png
-
45components/Collapsible.tsx
-
24components/ExternalLink.tsx
-
18components/HapticTab.tsx
-
40components/HelloWave.tsx
-
82components/ParallaxScrollView.tsx
-
60components/ThemedText.tsx
-
14components/ThemedView.tsx
-
10components/__tests__/ThemedText-test.tsx
-
24components/__tests__/__snapshots__/ThemedText-test.tsx.snap
-
32components/ui/IconSymbol.ios.tsx
-
43components/ui/IconSymbol.tsx
-
22components/ui/TabBarBackground.ios.tsx
-
6components/ui/TabBarBackground.tsx
-
26constants/Colors.ts
-
1hooks/useColorScheme.ts
-
21hooks/useColorScheme.web.ts
-
21hooks/useThemeColor.ts
-
14386package-lock.json
-
54package.json
-
112scripts/reset-project.js
-
17tsconfig.json
@ -0,0 +1,38 @@ |
|||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files |
||||
|
|
||||
|
# dependencies |
||||
|
node_modules/ |
||||
|
|
||||
|
# Expo |
||||
|
.expo/ |
||||
|
dist/ |
||||
|
web-build/ |
||||
|
expo-env.d.ts |
||||
|
|
||||
|
# Native |
||||
|
*.orig.* |
||||
|
*.jks |
||||
|
*.p8 |
||||
|
*.p12 |
||||
|
*.key |
||||
|
*.mobileprovision |
||||
|
|
||||
|
# Metro |
||||
|
.metro-health-check* |
||||
|
|
||||
|
# debug |
||||
|
npm-debug.* |
||||
|
yarn-debug.* |
||||
|
yarn-error.* |
||||
|
|
||||
|
# macOS |
||||
|
.DS_Store |
||||
|
*.pem |
||||
|
|
||||
|
# local env files |
||||
|
.env*.local |
||||
|
|
||||
|
# typescript |
||||
|
*.tsbuildinfo |
||||
|
|
||||
|
app-example |
@ -0,0 +1,50 @@ |
|||||
|
# Welcome to your Expo app 👋 |
||||
|
|
||||
|
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). |
||||
|
|
||||
|
## Get started |
||||
|
|
||||
|
1. Install dependencies |
||||
|
|
||||
|
```bash |
||||
|
npm install |
||||
|
``` |
||||
|
|
||||
|
2. Start the app |
||||
|
|
||||
|
```bash |
||||
|
npx expo start |
||||
|
``` |
||||
|
|
||||
|
In the output, you'll find options to open the app in a |
||||
|
|
||||
|
- [development build](https://docs.expo.dev/develop/development-builds/introduction/) |
||||
|
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) |
||||
|
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) |
||||
|
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo |
||||
|
|
||||
|
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). |
||||
|
|
||||
|
## Get a fresh project |
||||
|
|
||||
|
When you're ready, run: |
||||
|
|
||||
|
```bash |
||||
|
npm run reset-project |
||||
|
``` |
||||
|
|
||||
|
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. |
||||
|
|
||||
|
## Learn more |
||||
|
|
||||
|
To learn more about developing your project with Expo, look at the following resources: |
||||
|
|
||||
|
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). |
||||
|
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. |
||||
|
|
||||
|
## Join the community |
||||
|
|
||||
|
Join our community of developers creating universal apps. |
||||
|
|
||||
|
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. |
||||
|
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. |
@ -0,0 +1,41 @@ |
|||||
|
{ |
||||
|
"expo": { |
||||
|
"name": "outline-rn", |
||||
|
"slug": "outline-rn", |
||||
|
"version": "1.0.0", |
||||
|
"orientation": "portrait", |
||||
|
"icon": "./assets/images/icon.png", |
||||
|
"scheme": "myapp", |
||||
|
"userInterfaceStyle": "automatic", |
||||
|
"newArchEnabled": true, |
||||
|
"ios": { |
||||
|
"supportsTablet": true |
||||
|
}, |
||||
|
"android": { |
||||
|
"adaptiveIcon": { |
||||
|
"foregroundImage": "./assets/images/adaptive-icon.png", |
||||
|
"backgroundColor": "#ffffff" |
||||
|
} |
||||
|
}, |
||||
|
"web": { |
||||
|
"bundler": "metro", |
||||
|
"output": "static", |
||||
|
"favicon": "./assets/images/favicon.png" |
||||
|
}, |
||||
|
"plugins": [ |
||||
|
"expo-router", |
||||
|
[ |
||||
|
"expo-splash-screen", |
||||
|
{ |
||||
|
"image": "./assets/images/splash-icon.png", |
||||
|
"imageWidth": 200, |
||||
|
"resizeMode": "contain", |
||||
|
"backgroundColor": "#ffffff" |
||||
|
} |
||||
|
] |
||||
|
], |
||||
|
"experiments": { |
||||
|
"typedRoutes": true |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,45 @@ |
|||||
|
import { Tabs } from 'expo-router'; |
||||
|
import React from 'react'; |
||||
|
import { Platform } from 'react-native'; |
||||
|
|
||||
|
import { HapticTab } from '@/components/HapticTab'; |
||||
|
import { IconSymbol } from '@/components/ui/IconSymbol'; |
||||
|
import TabBarBackground from '@/components/ui/TabBarBackground'; |
||||
|
import { Colors } from '@/constants/Colors'; |
||||
|
import { useColorScheme } from '@/hooks/useColorScheme'; |
||||
|
|
||||
|
export default function TabLayout() { |
||||
|
const colorScheme = useColorScheme(); |
||||
|
|
||||
|
return ( |
||||
|
<Tabs |
||||
|
screenOptions={{ |
||||
|
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, |
||||
|
headerShown: false, |
||||
|
tabBarButton: HapticTab, |
||||
|
tabBarBackground: TabBarBackground, |
||||
|
tabBarStyle: Platform.select({ |
||||
|
ios: { |
||||
|
// Use a transparent background on iOS to show the blur effect
|
||||
|
position: 'absolute', |
||||
|
}, |
||||
|
default: {}, |
||||
|
}), |
||||
|
}}> |
||||
|
<Tabs.Screen |
||||
|
name="index" |
||||
|
options={{ |
||||
|
title: 'Home', |
||||
|
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />, |
||||
|
}} |
||||
|
/> |
||||
|
<Tabs.Screen |
||||
|
name="explore" |
||||
|
options={{ |
||||
|
title: 'Explore', |
||||
|
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, |
||||
|
}} |
||||
|
/> |
||||
|
</Tabs> |
||||
|
); |
||||
|
} |
@ -0,0 +1,109 @@ |
|||||
|
import { StyleSheet, Image, Platform } from 'react-native'; |
||||
|
|
||||
|
import { Collapsible } from '@/components/Collapsible'; |
||||
|
import { ExternalLink } from '@/components/ExternalLink'; |
||||
|
import ParallaxScrollView from '@/components/ParallaxScrollView'; |
||||
|
import { ThemedText } from '@/components/ThemedText'; |
||||
|
import { ThemedView } from '@/components/ThemedView'; |
||||
|
import { IconSymbol } from '@/components/ui/IconSymbol'; |
||||
|
|
||||
|
export default function TabTwoScreen() { |
||||
|
return ( |
||||
|
<ParallaxScrollView |
||||
|
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }} |
||||
|
headerImage={ |
||||
|
<IconSymbol |
||||
|
size={310} |
||||
|
color="#808080" |
||||
|
name="chevron.left.forwardslash.chevron.right" |
||||
|
style={styles.headerImage} |
||||
|
/> |
||||
|
}> |
||||
|
<ThemedView style={styles.titleContainer}> |
||||
|
<ThemedText type="title">Explore</ThemedText> |
||||
|
</ThemedView> |
||||
|
<ThemedText>This app includes example code to help you get started.</ThemedText> |
||||
|
<Collapsible title="File-based routing"> |
||||
|
<ThemedText> |
||||
|
This app has two screens:{' '} |
||||
|
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '} |
||||
|
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText> |
||||
|
</ThemedText> |
||||
|
<ThemedText> |
||||
|
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '} |
||||
|
sets up the tab navigator. |
||||
|
</ThemedText> |
||||
|
<ExternalLink href="https://docs.expo.dev/router/introduction"> |
||||
|
<ThemedText type="link">Learn more</ThemedText> |
||||
|
</ExternalLink> |
||||
|
</Collapsible> |
||||
|
<Collapsible title="Android, iOS, and web support"> |
||||
|
<ThemedText> |
||||
|
You can open this project on Android, iOS, and the web. To open the web version, press{' '} |
||||
|
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project. |
||||
|
</ThemedText> |
||||
|
</Collapsible> |
||||
|
<Collapsible title="Images"> |
||||
|
<ThemedText> |
||||
|
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '} |
||||
|
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for |
||||
|
different screen densities |
||||
|
</ThemedText> |
||||
|
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} /> |
||||
|
<ExternalLink href="https://reactnative.dev/docs/images"> |
||||
|
<ThemedText type="link">Learn more</ThemedText> |
||||
|
</ExternalLink> |
||||
|
</Collapsible> |
||||
|
<Collapsible title="Custom fonts"> |
||||
|
<ThemedText> |
||||
|
Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '} |
||||
|
<ThemedText style={{ fontFamily: 'SpaceMono' }}> |
||||
|
custom fonts such as this one. |
||||
|
</ThemedText> |
||||
|
</ThemedText> |
||||
|
<ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font"> |
||||
|
<ThemedText type="link">Learn more</ThemedText> |
||||
|
</ExternalLink> |
||||
|
</Collapsible> |
||||
|
<Collapsible title="Light and dark mode components"> |
||||
|
<ThemedText> |
||||
|
This template has light and dark mode support. The{' '} |
||||
|
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect |
||||
|
what the user's current color scheme is, and so you can adjust UI colors accordingly. |
||||
|
</ThemedText> |
||||
|
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/"> |
||||
|
<ThemedText type="link">Learn more</ThemedText> |
||||
|
</ExternalLink> |
||||
|
</Collapsible> |
||||
|
<Collapsible title="Animations"> |
||||
|
<ThemedText> |
||||
|
This template includes an example of an animated component. The{' '} |
||||
|
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses |
||||
|
the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '} |
||||
|
library to create a waving hand animation. |
||||
|
</ThemedText> |
||||
|
{Platform.select({ |
||||
|
ios: ( |
||||
|
<ThemedText> |
||||
|
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '} |
||||
|
component provides a parallax effect for the header image. |
||||
|
</ThemedText> |
||||
|
), |
||||
|
})} |
||||
|
</Collapsible> |
||||
|
</ParallaxScrollView> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
headerImage: { |
||||
|
color: '#808080', |
||||
|
bottom: -90, |
||||
|
left: -35, |
||||
|
position: 'absolute', |
||||
|
}, |
||||
|
titleContainer: { |
||||
|
flexDirection: 'row', |
||||
|
gap: 8, |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,74 @@ |
|||||
|
import { Image, StyleSheet, Platform } from 'react-native'; |
||||
|
|
||||
|
import { HelloWave } from '@/components/HelloWave'; |
||||
|
import ParallaxScrollView from '@/components/ParallaxScrollView'; |
||||
|
import { ThemedText } from '@/components/ThemedText'; |
||||
|
import { ThemedView } from '@/components/ThemedView'; |
||||
|
|
||||
|
export default function HomeScreen() { |
||||
|
return ( |
||||
|
<ParallaxScrollView |
||||
|
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} |
||||
|
headerImage={ |
||||
|
<Image |
||||
|
source={require('@/assets/images/partial-react-logo.png')} |
||||
|
style={styles.reactLogo} |
||||
|
/> |
||||
|
}> |
||||
|
<ThemedView style={styles.titleContainer}> |
||||
|
<ThemedText type="title">Welcome!</ThemedText> |
||||
|
<HelloWave /> |
||||
|
</ThemedView> |
||||
|
<ThemedView style={styles.stepContainer}> |
||||
|
<ThemedText type="subtitle">Step 1: Try it</ThemedText> |
||||
|
<ThemedText> |
||||
|
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes. |
||||
|
Press{' '} |
||||
|
<ThemedText type="defaultSemiBold"> |
||||
|
{Platform.select({ |
||||
|
ios: 'cmd + d', |
||||
|
android: 'cmd + m', |
||||
|
web: 'F12' |
||||
|
})} |
||||
|
</ThemedText>{' '} |
||||
|
to open developer tools. |
||||
|
</ThemedText> |
||||
|
</ThemedView> |
||||
|
<ThemedView style={styles.stepContainer}> |
||||
|
<ThemedText type="subtitle">Step 2: Explore</ThemedText> |
||||
|
<ThemedText> |
||||
|
Tap the Explore tab to learn more about what's included in this starter app. |
||||
|
</ThemedText> |
||||
|
</ThemedView> |
||||
|
<ThemedView style={styles.stepContainer}> |
||||
|
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText> |
||||
|
<ThemedText> |
||||
|
When you're ready, run{' '} |
||||
|
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '} |
||||
|
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '} |
||||
|
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '} |
||||
|
<ThemedText type="defaultSemiBold">app-example</ThemedText>. |
||||
|
</ThemedText> |
||||
|
</ThemedView> |
||||
|
</ParallaxScrollView> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
titleContainer: { |
||||
|
flexDirection: 'row', |
||||
|
alignItems: 'center', |
||||
|
gap: 8, |
||||
|
}, |
||||
|
stepContainer: { |
||||
|
gap: 8, |
||||
|
marginBottom: 8, |
||||
|
}, |
||||
|
reactLogo: { |
||||
|
height: 178, |
||||
|
width: 290, |
||||
|
bottom: 0, |
||||
|
left: 0, |
||||
|
position: 'absolute', |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,32 @@ |
|||||
|
import { Link, Stack } from 'expo-router'; |
||||
|
import { StyleSheet } from 'react-native'; |
||||
|
|
||||
|
import { ThemedText } from '@/components/ThemedText'; |
||||
|
import { ThemedView } from '@/components/ThemedView'; |
||||
|
|
||||
|
export default function NotFoundScreen() { |
||||
|
return ( |
||||
|
<> |
||||
|
<Stack.Screen options={{ title: 'Oops!' }} /> |
||||
|
<ThemedView style={styles.container}> |
||||
|
<ThemedText type="title">This screen doesn't exist.</ThemedText> |
||||
|
<Link href="/" style={styles.link}> |
||||
|
<ThemedText type="link">Go to home screen!</ThemedText> |
||||
|
</Link> |
||||
|
</ThemedView> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
container: { |
||||
|
flex: 1, |
||||
|
alignItems: 'center', |
||||
|
justifyContent: 'center', |
||||
|
padding: 20, |
||||
|
}, |
||||
|
link: { |
||||
|
marginTop: 15, |
||||
|
paddingVertical: 15, |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,39 @@ |
|||||
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; |
||||
|
import { useFonts } from 'expo-font'; |
||||
|
import { Stack } from 'expo-router'; |
||||
|
import * as SplashScreen from 'expo-splash-screen'; |
||||
|
import { StatusBar } from 'expo-status-bar'; |
||||
|
import { useEffect } from 'react'; |
||||
|
import 'react-native-reanimated'; |
||||
|
|
||||
|
import { useColorScheme } from '@/hooks/useColorScheme'; |
||||
|
|
||||
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
|
SplashScreen.preventAutoHideAsync(); |
||||
|
|
||||
|
export default function RootLayout() { |
||||
|
const colorScheme = useColorScheme(); |
||||
|
const [loaded] = useFonts({ |
||||
|
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), |
||||
|
}); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (loaded) { |
||||
|
SplashScreen.hideAsync(); |
||||
|
} |
||||
|
}, [loaded]); |
||||
|
|
||||
|
if (!loaded) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> |
||||
|
<Stack> |
||||
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> |
||||
|
<Stack.Screen name="+not-found" /> |
||||
|
</Stack> |
||||
|
<StatusBar style="auto" /> |
||||
|
</ThemeProvider> |
||||
|
); |
||||
|
} |
After Width: 1024 | Height: 1024 | Size: 17 KiB |
After Width: 48 | Height: 48 | Size: 1.4 KiB |
After Width: 1024 | Height: 1024 | Size: 22 KiB |
After Width: 518 | Height: 316 | Size: 5.0 KiB |
After Width: 100 | Height: 100 | Size: 6.2 KiB |
After Width: 200 | Height: 200 | Size: 14 KiB |
After Width: 300 | Height: 300 | Size: 21 KiB |
After Width: 1024 | Height: 1024 | Size: 17 KiB |
@ -0,0 +1,45 @@ |
|||||
|
import { PropsWithChildren, useState } from 'react'; |
||||
|
import { StyleSheet, TouchableOpacity } from 'react-native'; |
||||
|
|
||||
|
import { ThemedText } from '@/components/ThemedText'; |
||||
|
import { ThemedView } from '@/components/ThemedView'; |
||||
|
import { IconSymbol } from '@/components/ui/IconSymbol'; |
||||
|
import { Colors } from '@/constants/Colors'; |
||||
|
import { useColorScheme } from '@/hooks/useColorScheme'; |
||||
|
|
||||
|
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) { |
||||
|
const [isOpen, setIsOpen] = useState(false); |
||||
|
const theme = useColorScheme() ?? 'light'; |
||||
|
|
||||
|
return ( |
||||
|
<ThemedView> |
||||
|
<TouchableOpacity |
||||
|
style={styles.heading} |
||||
|
onPress={() => setIsOpen((value) => !value)} |
||||
|
activeOpacity={0.8}> |
||||
|
<IconSymbol |
||||
|
name="chevron.right" |
||||
|
size={18} |
||||
|
weight="medium" |
||||
|
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon} |
||||
|
style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }} |
||||
|
/> |
||||
|
|
||||
|
<ThemedText type="defaultSemiBold">{title}</ThemedText> |
||||
|
</TouchableOpacity> |
||||
|
{isOpen && <ThemedView style={styles.content}>{children}</ThemedView>} |
||||
|
</ThemedView> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
heading: { |
||||
|
flexDirection: 'row', |
||||
|
alignItems: 'center', |
||||
|
gap: 6, |
||||
|
}, |
||||
|
content: { |
||||
|
marginTop: 6, |
||||
|
marginLeft: 24, |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,24 @@ |
|||||
|
import { Link } from 'expo-router'; |
||||
|
import { openBrowserAsync } from 'expo-web-browser'; |
||||
|
import { type ComponentProps } from 'react'; |
||||
|
import { Platform } from 'react-native'; |
||||
|
|
||||
|
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: string }; |
||||
|
|
||||
|
export function ExternalLink({ href, ...rest }: Props) { |
||||
|
return ( |
||||
|
<Link |
||||
|
target="_blank" |
||||
|
{...rest} |
||||
|
href={href} |
||||
|
onPress={async (event) => { |
||||
|
if (Platform.OS !== 'web') { |
||||
|
// Prevent the default behavior of linking to the default browser on native.
|
||||
|
event.preventDefault(); |
||||
|
// Open the link in an in-app browser.
|
||||
|
await openBrowserAsync(href); |
||||
|
} |
||||
|
}} |
||||
|
/> |
||||
|
); |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; |
||||
|
import { PlatformPressable } from '@react-navigation/elements'; |
||||
|
import * as Haptics from 'expo-haptics'; |
||||
|
|
||||
|
export function HapticTab(props: BottomTabBarButtonProps) { |
||||
|
return ( |
||||
|
<PlatformPressable |
||||
|
{...props} |
||||
|
onPressIn={(ev) => { |
||||
|
if (process.env.EXPO_OS === 'ios') { |
||||
|
// Add a soft haptic feedback when pressing down on the tabs.
|
||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
||||
|
} |
||||
|
props.onPressIn?.(ev); |
||||
|
}} |
||||
|
/> |
||||
|
); |
||||
|
} |
@ -0,0 +1,40 @@ |
|||||
|
import { useEffect } from 'react'; |
||||
|
import { StyleSheet } from 'react-native'; |
||||
|
import Animated, { |
||||
|
useSharedValue, |
||||
|
useAnimatedStyle, |
||||
|
withTiming, |
||||
|
withRepeat, |
||||
|
withSequence, |
||||
|
} from 'react-native-reanimated'; |
||||
|
|
||||
|
import { ThemedText } from '@/components/ThemedText'; |
||||
|
|
||||
|
export function HelloWave() { |
||||
|
const rotationAnimation = useSharedValue(0); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
rotationAnimation.value = withRepeat( |
||||
|
withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), |
||||
|
4 // Run the animation 4 times
|
||||
|
); |
||||
|
}, []); |
||||
|
|
||||
|
const animatedStyle = useAnimatedStyle(() => ({ |
||||
|
transform: [{ rotate: `${rotationAnimation.value}deg` }], |
||||
|
})); |
||||
|
|
||||
|
return ( |
||||
|
<Animated.View style={animatedStyle}> |
||||
|
<ThemedText style={styles.text}>👋</ThemedText> |
||||
|
</Animated.View> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
text: { |
||||
|
fontSize: 28, |
||||
|
lineHeight: 32, |
||||
|
marginTop: -6, |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,82 @@ |
|||||
|
import type { PropsWithChildren, ReactElement } from 'react'; |
||||
|
import { StyleSheet } from 'react-native'; |
||||
|
import Animated, { |
||||
|
interpolate, |
||||
|
useAnimatedRef, |
||||
|
useAnimatedStyle, |
||||
|
useScrollViewOffset, |
||||
|
} from 'react-native-reanimated'; |
||||
|
|
||||
|
import { ThemedView } from '@/components/ThemedView'; |
||||
|
import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'; |
||||
|
import { useColorScheme } from '@/hooks/useColorScheme'; |
||||
|
|
||||
|
const HEADER_HEIGHT = 250; |
||||
|
|
||||
|
type Props = PropsWithChildren<{ |
||||
|
headerImage: ReactElement; |
||||
|
headerBackgroundColor: { dark: string; light: string }; |
||||
|
}>; |
||||
|
|
||||
|
export default function ParallaxScrollView({ |
||||
|
children, |
||||
|
headerImage, |
||||
|
headerBackgroundColor, |
||||
|
}: Props) { |
||||
|
const colorScheme = useColorScheme() ?? 'light'; |
||||
|
const scrollRef = useAnimatedRef<Animated.ScrollView>(); |
||||
|
const scrollOffset = useScrollViewOffset(scrollRef); |
||||
|
const bottom = useBottomTabOverflow(); |
||||
|
const headerAnimatedStyle = useAnimatedStyle(() => { |
||||
|
return { |
||||
|
transform: [ |
||||
|
{ |
||||
|
translateY: interpolate( |
||||
|
scrollOffset.value, |
||||
|
[-HEADER_HEIGHT, 0, HEADER_HEIGHT], |
||||
|
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<ThemedView style={styles.container}> |
||||
|
<Animated.ScrollView |
||||
|
ref={scrollRef} |
||||
|
scrollEventThrottle={16} |
||||
|
scrollIndicatorInsets={{ bottom }} |
||||
|
contentContainerStyle={{ paddingBottom: bottom }}> |
||||
|
<Animated.View |
||||
|
style={[ |
||||
|
styles.header, |
||||
|
{ backgroundColor: headerBackgroundColor[colorScheme] }, |
||||
|
headerAnimatedStyle, |
||||
|
]}> |
||||
|
{headerImage} |
||||
|
</Animated.View> |
||||
|
<ThemedView style={styles.content}>{children}</ThemedView> |
||||
|
</Animated.ScrollView> |
||||
|
</ThemedView> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
container: { |
||||
|
flex: 1, |
||||
|
}, |
||||
|
header: { |
||||
|
height: HEADER_HEIGHT, |
||||
|
overflow: 'hidden', |
||||
|
}, |
||||
|
content: { |
||||
|
flex: 1, |
||||
|
padding: 32, |
||||
|
gap: 16, |
||||
|
overflow: 'hidden', |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,60 @@ |
|||||
|
import { Text, type TextProps, StyleSheet } from 'react-native'; |
||||
|
|
||||
|
import { useThemeColor } from '@/hooks/useThemeColor'; |
||||
|
|
||||
|
export type ThemedTextProps = TextProps & { |
||||
|
lightColor?: string; |
||||
|
darkColor?: string; |
||||
|
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; |
||||
|
}; |
||||
|
|
||||
|
export function ThemedText({ |
||||
|
style, |
||||
|
lightColor, |
||||
|
darkColor, |
||||
|
type = 'default', |
||||
|
...rest |
||||
|
}: ThemedTextProps) { |
||||
|
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); |
||||
|
|
||||
|
return ( |
||||
|
<Text |
||||
|
style={[ |
||||
|
{ color }, |
||||
|
type === 'default' ? styles.default : undefined, |
||||
|
type === 'title' ? styles.title : undefined, |
||||
|
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, |
||||
|
type === 'subtitle' ? styles.subtitle : undefined, |
||||
|
type === 'link' ? styles.link : undefined, |
||||
|
style, |
||||
|
]} |
||||
|
{...rest} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
default: { |
||||
|
fontSize: 16, |
||||
|
lineHeight: 24, |
||||
|
}, |
||||
|
defaultSemiBold: { |
||||
|
fontSize: 16, |
||||
|
lineHeight: 24, |
||||
|
fontWeight: '600', |
||||
|
}, |
||||
|
title: { |
||||
|
fontSize: 32, |
||||
|
fontWeight: 'bold', |
||||
|
lineHeight: 32, |
||||
|
}, |
||||
|
subtitle: { |
||||
|
fontSize: 20, |
||||
|
fontWeight: 'bold', |
||||
|
}, |
||||
|
link: { |
||||
|
lineHeight: 30, |
||||
|
fontSize: 16, |
||||
|
color: '#0a7ea4', |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,14 @@ |
|||||
|
import { View, type ViewProps } from 'react-native'; |
||||
|
|
||||
|
import { useThemeColor } from '@/hooks/useThemeColor'; |
||||
|
|
||||
|
export type ThemedViewProps = ViewProps & { |
||||
|
lightColor?: string; |
||||
|
darkColor?: string; |
||||
|
}; |
||||
|
|
||||
|
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { |
||||
|
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); |
||||
|
|
||||
|
return <View style={[{ backgroundColor }, style]} {...otherProps} />; |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
import * as React from 'react'; |
||||
|
import renderer from 'react-test-renderer'; |
||||
|
|
||||
|
import { ThemedText } from '../ThemedText'; |
||||
|
|
||||
|
it(`renders correctly`, () => { |
||||
|
const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON(); |
||||
|
|
||||
|
expect(tree).toMatchSnapshot(); |
||||
|
}); |
@ -0,0 +1,24 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`renders correctly 1`] = ` |
||||
|
<Text |
||||
|
style={ |
||||
|
[ |
||||
|
{ |
||||
|
"color": "#11181C", |
||||
|
}, |
||||
|
{ |
||||
|
"fontSize": 16, |
||||
|
"lineHeight": 24, |
||||
|
}, |
||||
|
undefined, |
||||
|
undefined, |
||||
|
undefined, |
||||
|
undefined, |
||||
|
undefined, |
||||
|
] |
||||
|
} |
||||
|
> |
||||
|
Snapshot test! |
||||
|
</Text> |
||||
|
`; |
@ -0,0 +1,32 @@ |
|||||
|
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; |
||||
|
import { StyleProp, ViewStyle } from 'react-native'; |
||||
|
|
||||
|
export function IconSymbol({ |
||||
|
name, |
||||
|
size = 24, |
||||
|
color, |
||||
|
style, |
||||
|
weight = 'regular', |
||||
|
}: { |
||||
|
name: SymbolViewProps['name']; |
||||
|
size?: number; |
||||
|
color: string; |
||||
|
style?: StyleProp<ViewStyle>; |
||||
|
weight?: SymbolWeight; |
||||
|
}) { |
||||
|
return ( |
||||
|
<SymbolView |
||||
|
weight={weight} |
||||
|
tintColor={color} |
||||
|
resizeMode="scaleAspectFit" |
||||
|
name={name} |
||||
|
style={[ |
||||
|
{ |
||||
|
width: size, |
||||
|
height: size, |
||||
|
}, |
||||
|
style, |
||||
|
]} |
||||
|
/> |
||||
|
); |
||||
|
} |
@ -0,0 +1,43 @@ |
|||||
|
// This file is a fallback for using MaterialIcons on Android and web.
|
||||
|
|
||||
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; |
||||
|
import { SymbolWeight } from 'expo-symbols'; |
||||
|
import React from 'react'; |
||||
|
import { OpaqueColorValue, StyleProp, ViewStyle } from 'react-native'; |
||||
|
|
||||
|
// Add your SFSymbol to MaterialIcons mappings here.
|
||||
|
const MAPPING = { |
||||
|
// See MaterialIcons here: https://icons.expo.fyi
|
||||
|
// See SF Symbols in the SF Symbols app on Mac.
|
||||
|
'house.fill': 'home', |
||||
|
'paperplane.fill': 'send', |
||||
|
'chevron.left.forwardslash.chevron.right': 'code', |
||||
|
'chevron.right': 'chevron-right', |
||||
|
} as Partial< |
||||
|
Record< |
||||
|
import('expo-symbols').SymbolViewProps['name'], |
||||
|
React.ComponentProps<typeof MaterialIcons>['name'] |
||||
|
> |
||||
|
>; |
||||
|
|
||||
|
export type IconSymbolName = keyof typeof MAPPING; |
||||
|
|
||||
|
/** |
||||
|
* An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage. |
||||
|
* |
||||
|
* Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons. |
||||
|
*/ |
||||
|
export function IconSymbol({ |
||||
|
name, |
||||
|
size = 24, |
||||
|
color, |
||||
|
style, |
||||
|
}: { |
||||
|
name: IconSymbolName; |
||||
|
size?: number; |
||||
|
color: string | OpaqueColorValue; |
||||
|
style?: StyleProp<ViewStyle>; |
||||
|
weight?: SymbolWeight; |
||||
|
}) { |
||||
|
return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />; |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; |
||||
|
import { BlurView } from 'expo-blur'; |
||||
|
import { StyleSheet } from 'react-native'; |
||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'; |
||||
|
|
||||
|
export default function BlurTabBarBackground() { |
||||
|
return ( |
||||
|
<BlurView |
||||
|
// System chrome material automatically adapts to the system's theme
|
||||
|
// and matches the native tab bar appearance on iOS.
|
||||
|
tint="systemChromeMaterial" |
||||
|
intensity={100} |
||||
|
style={StyleSheet.absoluteFill} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export function useBottomTabOverflow() { |
||||
|
const tabHeight = useBottomTabBarHeight(); |
||||
|
const { bottom } = useSafeAreaInsets(); |
||||
|
return tabHeight - bottom; |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
// This is a shim for web and Android where the tab bar is generally opaque.
|
||||
|
export default undefined; |
||||
|
|
||||
|
export function useBottomTabOverflow() { |
||||
|
return 0; |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
/** |
||||
|
* Below are the colors that are used in the app. The colors are defined in the light and dark mode. |
||||
|
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
|
*/ |
||||
|
|
||||
|
const tintColorLight = '#0a7ea4'; |
||||
|
const tintColorDark = '#fff'; |
||||
|
|
||||
|
export const Colors = { |
||||
|
light: { |
||||
|
text: '#11181C', |
||||
|
background: '#fff', |
||||
|
tint: tintColorLight, |
||||
|
icon: '#687076', |
||||
|
tabIconDefault: '#687076', |
||||
|
tabIconSelected: tintColorLight, |
||||
|
}, |
||||
|
dark: { |
||||
|
text: '#ECEDEE', |
||||
|
background: '#151718', |
||||
|
tint: tintColorDark, |
||||
|
icon: '#9BA1A6', |
||||
|
tabIconDefault: '#9BA1A6', |
||||
|
tabIconSelected: tintColorDark, |
||||
|
}, |
||||
|
}; |
@ -0,0 +1 @@ |
|||||
|
export { useColorScheme } from 'react-native'; |
@ -0,0 +1,21 @@ |
|||||
|
import { useEffect, useState } from 'react'; |
||||
|
import { useColorScheme as useRNColorScheme } from 'react-native'; |
||||
|
|
||||
|
/** |
||||
|
* To support static rendering, this value needs to be re-calculated on the client side for web |
||||
|
*/ |
||||
|
export function useColorScheme() { |
||||
|
const [hasHydrated, setHasHydrated] = useState(false); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
setHasHydrated(true); |
||||
|
}, []); |
||||
|
|
||||
|
const colorScheme = useRNColorScheme(); |
||||
|
|
||||
|
if (hasHydrated) { |
||||
|
return colorScheme; |
||||
|
} |
||||
|
|
||||
|
return 'light'; |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
/** |
||||
|
* Learn more about light and dark modes: |
||||
|
* https://docs.expo.dev/guides/color-schemes/
|
||||
|
*/ |
||||
|
|
||||
|
import { Colors } from '@/constants/Colors'; |
||||
|
import { useColorScheme } from '@/hooks/useColorScheme'; |
||||
|
|
||||
|
export function useThemeColor( |
||||
|
props: { light?: string; dark?: string }, |
||||
|
colorName: keyof typeof Colors.light & keyof typeof Colors.dark |
||||
|
) { |
||||
|
const theme = useColorScheme() ?? 'light'; |
||||
|
const colorFromProps = props[theme]; |
||||
|
|
||||
|
if (colorFromProps) { |
||||
|
return colorFromProps; |
||||
|
} else { |
||||
|
return Colors[theme][colorName]; |
||||
|
} |
||||
|
} |
14386
package-lock.json
File diff suppressed because it is too large
View File
@ -0,0 +1,54 @@ |
|||||
|
{ |
||||
|
"name": "outline-rn", |
||||
|
"main": "expo-router/entry", |
||||
|
"version": "1.0.0", |
||||
|
"scripts": { |
||||
|
"start": "expo start", |
||||
|
"reset-project": "node ./scripts/reset-project.js", |
||||
|
"android": "expo start --android", |
||||
|
"ios": "expo start --ios", |
||||
|
"web": "expo start --web", |
||||
|
"test": "jest --watchAll", |
||||
|
"lint": "expo lint" |
||||
|
}, |
||||
|
"jest": { |
||||
|
"preset": "jest-expo" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@expo/vector-icons": "^14.0.2", |
||||
|
"@react-navigation/bottom-tabs": "^7.2.0", |
||||
|
"@react-navigation/native": "^7.0.14", |
||||
|
"expo": "~52.0.38", |
||||
|
"expo-blur": "~14.0.3", |
||||
|
"expo-constants": "~17.0.8", |
||||
|
"expo-font": "~13.0.4", |
||||
|
"expo-haptics": "~14.0.1", |
||||
|
"expo-linking": "~7.0.5", |
||||
|
"expo-router": "~4.0.18", |
||||
|
"expo-splash-screen": "~0.29.22", |
||||
|
"expo-status-bar": "~2.0.1", |
||||
|
"expo-symbols": "~0.2.2", |
||||
|
"expo-system-ui": "~4.0.8", |
||||
|
"expo-web-browser": "~14.0.2", |
||||
|
"react": "18.3.1", |
||||
|
"react-dom": "18.3.1", |
||||
|
"react-native": "0.76.7", |
||||
|
"react-native-gesture-handler": "~2.20.2", |
||||
|
"react-native-reanimated": "~3.16.1", |
||||
|
"react-native-safe-area-context": "4.12.0", |
||||
|
"react-native-screens": "~4.4.0", |
||||
|
"react-native-web": "~0.19.13", |
||||
|
"react-native-webview": "13.12.5" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@babel/core": "^7.25.2", |
||||
|
"@types/jest": "^29.5.12", |
||||
|
"@types/react": "~18.3.12", |
||||
|
"@types/react-test-renderer": "^18.3.0", |
||||
|
"jest": "^29.2.1", |
||||
|
"jest-expo": "~52.0.6", |
||||
|
"react-test-renderer": "18.3.1", |
||||
|
"typescript": "^5.3.3" |
||||
|
}, |
||||
|
"private": true |
||||
|
} |
@ -0,0 +1,112 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
/** |
||||
|
* This script is used to reset the project to a blank state. |
||||
|
* It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. |
||||
|
* You can remove the `reset-project` script from package.json and safely delete this file after running it. |
||||
|
*/ |
||||
|
|
||||
|
const fs = require("fs"); |
||||
|
const path = require("path"); |
||||
|
const readline = require("readline"); |
||||
|
|
||||
|
const root = process.cwd(); |
||||
|
const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; |
||||
|
const exampleDir = "app-example"; |
||||
|
const newAppDir = "app"; |
||||
|
const exampleDirPath = path.join(root, exampleDir); |
||||
|
|
||||
|
const indexContent = `import { Text, View } from "react-native";
|
||||
|
|
||||
|
export default function Index() { |
||||
|
return ( |
||||
|
<View |
||||
|
style={{ |
||||
|
flex: 1, |
||||
|
justifyContent: "center", |
||||
|
alignItems: "center", |
||||
|
}} |
||||
|
> |
||||
|
<Text>Edit app/index.tsx to edit this screen.</Text> |
||||
|
</View> |
||||
|
); |
||||
|
} |
||||
|
`;
|
||||
|
|
||||
|
const layoutContent = `import { Stack } from "expo-router";
|
||||
|
|
||||
|
export default function RootLayout() { |
||||
|
return <Stack />; |
||||
|
} |
||||
|
`;
|
||||
|
|
||||
|
const rl = readline.createInterface({ |
||||
|
input: process.stdin, |
||||
|
output: process.stdout, |
||||
|
}); |
||||
|
|
||||
|
const moveDirectories = async (userInput) => { |
||||
|
try { |
||||
|
if (userInput === "y") { |
||||
|
// Create the app-example directory
|
||||
|
await fs.promises.mkdir(exampleDirPath, { recursive: true }); |
||||
|
console.log(`📁 /${exampleDir} directory created.`); |
||||
|
} |
||||
|
|
||||
|
// Move old directories to new app-example directory or delete them
|
||||
|
for (const dir of oldDirs) { |
||||
|
const oldDirPath = path.join(root, dir); |
||||
|
if (fs.existsSync(oldDirPath)) { |
||||
|
if (userInput === "y") { |
||||
|
const newDirPath = path.join(root, exampleDir, dir); |
||||
|
await fs.promises.rename(oldDirPath, newDirPath); |
||||
|
console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); |
||||
|
} else { |
||||
|
await fs.promises.rm(oldDirPath, { recursive: true, force: true }); |
||||
|
console.log(`❌ /${dir} deleted.`); |
||||
|
} |
||||
|
} else { |
||||
|
console.log(`➡️ /${dir} does not exist, skipping.`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Create new /app directory
|
||||
|
const newAppDirPath = path.join(root, newAppDir); |
||||
|
await fs.promises.mkdir(newAppDirPath, { recursive: true }); |
||||
|
console.log("\n📁 New /app directory created."); |
||||
|
|
||||
|
// Create index.tsx
|
||||
|
const indexPath = path.join(newAppDirPath, "index.tsx"); |
||||
|
await fs.promises.writeFile(indexPath, indexContent); |
||||
|
console.log("📄 app/index.tsx created."); |
||||
|
|
||||
|
// Create _layout.tsx
|
||||
|
const layoutPath = path.join(newAppDirPath, "_layout.tsx"); |
||||
|
await fs.promises.writeFile(layoutPath, layoutContent); |
||||
|
console.log("📄 app/_layout.tsx created."); |
||||
|
|
||||
|
console.log("\n✅ Project reset complete. Next steps:"); |
||||
|
console.log( |
||||
|
`1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ |
||||
|
userInput === "y" |
||||
|
? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` |
||||
|
: "" |
||||
|
}`
|
||||
|
); |
||||
|
} catch (error) { |
||||
|
console.error(`❌ Error during script execution: ${error.message}`); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
rl.question( |
||||
|
"Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", |
||||
|
(answer) => { |
||||
|
const userInput = answer.trim().toLowerCase() || "y"; |
||||
|
if (userInput === "y" || userInput === "n") { |
||||
|
moveDirectories(userInput).finally(() => rl.close()); |
||||
|
} else { |
||||
|
console.log("❌ Invalid input. Please enter 'Y' or 'N'."); |
||||
|
rl.close(); |
||||
|
} |
||||
|
} |
||||
|
); |
@ -0,0 +1,17 @@ |
|||||
|
{ |
||||
|
"extends": "expo/tsconfig.base", |
||||
|
"compilerOptions": { |
||||
|
"strict": true, |
||||
|
"paths": { |
||||
|
"@/*": [ |
||||
|
"./*" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"include": [ |
||||
|
"**/*.ts", |
||||
|
"**/*.tsx", |
||||
|
".expo/types/**/*.ts", |
||||
|
"expo-env.d.ts" |
||||
|
] |
||||
|
} |