An application to use Bluesky lexicons in a slightly different way

Initial commit

Generated by create-expo-app 3.5.3.

Tyler Cheek e49a92fa

+43
.gitignore
··· 1 + # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 + 3 + # dependencies 4 + node_modules/ 5 + 6 + # Expo 7 + .expo/ 8 + dist/ 9 + web-build/ 10 + expo-env.d.ts 11 + 12 + # Native 13 + .kotlin/ 14 + *.orig.* 15 + *.jks 16 + *.p8 17 + *.p12 18 + *.key 19 + *.mobileprovision 20 + 21 + # Metro 22 + .metro-health-check* 23 + 24 + # debug 25 + npm-debug.* 26 + yarn-debug.* 27 + yarn-error.* 28 + 29 + # macOS 30 + .DS_Store 31 + *.pem 32 + 33 + # local env files 34 + .env*.local 35 + 36 + # typescript 37 + *.tsbuildinfo 38 + 39 + app-example 40 + 41 + # generated native folders 42 + /ios 43 + /android
+1
.vscode/extensions.json
··· 1 + { "recommendations": ["expo.vscode-expo-tools"] }
+7
.vscode/settings.json
··· 1 + { 2 + "editor.codeActionsOnSave": { 3 + "source.fixAll": "explicit", 4 + "source.organizeImports": "explicit", 5 + "source.sortMembers": "explicit" 6 + } 7 + }
+50
README.md
··· 1 + # Welcome to your Expo app 👋 2 + 3 + This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 + 5 + ## Get started 6 + 7 + 1. Install dependencies 8 + 9 + ```bash 10 + npm install 11 + ``` 12 + 13 + 2. Start the app 14 + 15 + ```bash 16 + npx expo start 17 + ``` 18 + 19 + In the output, you'll find options to open the app in a 20 + 21 + - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 + - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 + - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 + - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 + 26 + You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 + 28 + ## Get a fresh project 29 + 30 + When you're ready, run: 31 + 32 + ```bash 33 + npm run reset-project 34 + ``` 35 + 36 + This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 + 38 + ## Learn more 39 + 40 + To learn more about developing your project with Expo, look at the following resources: 41 + 42 + - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 + - [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. 44 + 45 + ## Join the community 46 + 47 + Join our community of developers creating universal apps. 48 + 49 + - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 + - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
+48
app.json
··· 1 + { 2 + "expo": { 3 + "name": "rabbit", 4 + "slug": "rabbit", 5 + "version": "1.0.0", 6 + "orientation": "portrait", 7 + "icon": "./assets/images/icon.png", 8 + "scheme": "rabbit", 9 + "userInterfaceStyle": "automatic", 10 + "newArchEnabled": true, 11 + "ios": { 12 + "supportsTablet": true 13 + }, 14 + "android": { 15 + "adaptiveIcon": { 16 + "backgroundColor": "#E6F4FE", 17 + "foregroundImage": "./assets/images/android-icon-foreground.png", 18 + "backgroundImage": "./assets/images/android-icon-background.png", 19 + "monochromeImage": "./assets/images/android-icon-monochrome.png" 20 + }, 21 + "edgeToEdgeEnabled": true, 22 + "predictiveBackGestureEnabled": false 23 + }, 24 + "web": { 25 + "output": "static", 26 + "favicon": "./assets/images/favicon.png" 27 + }, 28 + "plugins": [ 29 + "expo-router", 30 + [ 31 + "expo-splash-screen", 32 + { 33 + "image": "./assets/images/splash-icon.png", 34 + "imageWidth": 200, 35 + "resizeMode": "contain", 36 + "backgroundColor": "#ffffff", 37 + "dark": { 38 + "backgroundColor": "#000000" 39 + } 40 + } 41 + ] 42 + ], 43 + "experiments": { 44 + "typedRoutes": true, 45 + "reactCompiler": true 46 + } 47 + } 48 + }
+35
app/(tabs)/_layout.tsx
··· 1 + import { Tabs } from 'expo-router'; 2 + import React from 'react'; 3 + 4 + import { HapticTab } from '@/components/haptic-tab'; 5 + import { IconSymbol } from '@/components/ui/icon-symbol'; 6 + import { Colors } from '@/constants/theme'; 7 + import { useColorScheme } from '@/hooks/use-color-scheme'; 8 + 9 + export default function TabLayout() { 10 + const colorScheme = useColorScheme(); 11 + 12 + return ( 13 + <Tabs 14 + screenOptions={{ 15 + tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, 16 + headerShown: false, 17 + tabBarButton: HapticTab, 18 + }}> 19 + <Tabs.Screen 20 + name="index" 21 + options={{ 22 + title: 'Home', 23 + tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />, 24 + }} 25 + /> 26 + <Tabs.Screen 27 + name="explore" 28 + options={{ 29 + title: 'Explore', 30 + tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, 31 + }} 32 + /> 33 + </Tabs> 34 + ); 35 + }
+112
app/(tabs)/explore.tsx
··· 1 + import { Image } from 'expo-image'; 2 + import { Platform, StyleSheet } from 'react-native'; 3 + 4 + import { Collapsible } from '@/components/ui/collapsible'; 5 + import { ExternalLink } from '@/components/external-link'; 6 + import ParallaxScrollView from '@/components/parallax-scroll-view'; 7 + import { ThemedText } from '@/components/themed-text'; 8 + import { ThemedView } from '@/components/themed-view'; 9 + import { IconSymbol } from '@/components/ui/icon-symbol'; 10 + import { Fonts } from '@/constants/theme'; 11 + 12 + export default function TabTwoScreen() { 13 + return ( 14 + <ParallaxScrollView 15 + headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }} 16 + headerImage={ 17 + <IconSymbol 18 + size={310} 19 + color="#808080" 20 + name="chevron.left.forwardslash.chevron.right" 21 + style={styles.headerImage} 22 + /> 23 + }> 24 + <ThemedView style={styles.titleContainer}> 25 + <ThemedText 26 + type="title" 27 + style={{ 28 + fontFamily: Fonts.rounded, 29 + }}> 30 + Explore 31 + </ThemedText> 32 + </ThemedView> 33 + <ThemedText>This app includes example code to help you get started.</ThemedText> 34 + <Collapsible title="File-based routing"> 35 + <ThemedText> 36 + This app has two screens:{' '} 37 + <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '} 38 + <ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText> 39 + </ThemedText> 40 + <ThemedText> 41 + The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '} 42 + sets up the tab navigator. 43 + </ThemedText> 44 + <ExternalLink href="https://docs.expo.dev/router/introduction"> 45 + <ThemedText type="link">Learn more</ThemedText> 46 + </ExternalLink> 47 + </Collapsible> 48 + <Collapsible title="Android, iOS, and web support"> 49 + <ThemedText> 50 + You can open this project on Android, iOS, and the web. To open the web version, press{' '} 51 + <ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project. 52 + </ThemedText> 53 + </Collapsible> 54 + <Collapsible title="Images"> 55 + <ThemedText> 56 + For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '} 57 + <ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for 58 + different screen densities 59 + </ThemedText> 60 + <Image 61 + source={require('@/assets/images/react-logo.png')} 62 + style={{ width: 100, height: 100, alignSelf: 'center' }} 63 + /> 64 + <ExternalLink href="https://reactnative.dev/docs/images"> 65 + <ThemedText type="link">Learn more</ThemedText> 66 + </ExternalLink> 67 + </Collapsible> 68 + <Collapsible title="Light and dark mode components"> 69 + <ThemedText> 70 + This template has light and dark mode support. The{' '} 71 + <ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect 72 + what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly. 73 + </ThemedText> 74 + <ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/"> 75 + <ThemedText type="link">Learn more</ThemedText> 76 + </ExternalLink> 77 + </Collapsible> 78 + <Collapsible title="Animations"> 79 + <ThemedText> 80 + This template includes an example of an animated component. The{' '} 81 + <ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses 82 + the powerful{' '} 83 + <ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}> 84 + react-native-reanimated 85 + </ThemedText>{' '} 86 + library to create a waving hand animation. 87 + </ThemedText> 88 + {Platform.select({ 89 + ios: ( 90 + <ThemedText> 91 + The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '} 92 + component provides a parallax effect for the header image. 93 + </ThemedText> 94 + ), 95 + })} 96 + </Collapsible> 97 + </ParallaxScrollView> 98 + ); 99 + } 100 + 101 + const styles = StyleSheet.create({ 102 + headerImage: { 103 + color: '#808080', 104 + bottom: -90, 105 + left: -35, 106 + position: 'absolute', 107 + }, 108 + titleContainer: { 109 + flexDirection: 'row', 110 + gap: 8, 111 + }, 112 + });
+98
app/(tabs)/index.tsx
··· 1 + import { Image } from 'expo-image'; 2 + import { Platform, StyleSheet } from 'react-native'; 3 + 4 + import { HelloWave } from '@/components/hello-wave'; 5 + import ParallaxScrollView from '@/components/parallax-scroll-view'; 6 + import { ThemedText } from '@/components/themed-text'; 7 + import { ThemedView } from '@/components/themed-view'; 8 + import { Link } from 'expo-router'; 9 + 10 + export default function HomeScreen() { 11 + return ( 12 + <ParallaxScrollView 13 + headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} 14 + headerImage={ 15 + <Image 16 + source={require('@/assets/images/partial-react-logo.png')} 17 + style={styles.reactLogo} 18 + /> 19 + }> 20 + <ThemedView style={styles.titleContainer}> 21 + <ThemedText type="title">Welcome!</ThemedText> 22 + <HelloWave /> 23 + </ThemedView> 24 + <ThemedView style={styles.stepContainer}> 25 + <ThemedText type="subtitle">Step 1: Try it</ThemedText> 26 + <ThemedText> 27 + Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes. 28 + Press{' '} 29 + <ThemedText type="defaultSemiBold"> 30 + {Platform.select({ 31 + ios: 'cmd + d', 32 + android: 'cmd + m', 33 + web: 'F12', 34 + })} 35 + </ThemedText>{' '} 36 + to open developer tools. 37 + </ThemedText> 38 + </ThemedView> 39 + <ThemedView style={styles.stepContainer}> 40 + <Link href="/modal"> 41 + <Link.Trigger> 42 + <ThemedText type="subtitle">Step 2: Explore</ThemedText> 43 + </Link.Trigger> 44 + <Link.Preview /> 45 + <Link.Menu> 46 + <Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} /> 47 + <Link.MenuAction 48 + title="Share" 49 + icon="square.and.arrow.up" 50 + onPress={() => alert('Share pressed')} 51 + /> 52 + <Link.Menu title="More" icon="ellipsis"> 53 + <Link.MenuAction 54 + title="Delete" 55 + icon="trash" 56 + destructive 57 + onPress={() => alert('Delete pressed')} 58 + /> 59 + </Link.Menu> 60 + </Link.Menu> 61 + </Link> 62 + 63 + <ThemedText> 64 + {`Tap the Explore tab to learn more about what's included in this starter app.`} 65 + </ThemedText> 66 + </ThemedView> 67 + <ThemedView style={styles.stepContainer}> 68 + <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText> 69 + <ThemedText> 70 + {`When you're ready, run `} 71 + <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '} 72 + <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '} 73 + <ThemedText type="defaultSemiBold">app</ThemedText> to{' '} 74 + <ThemedText type="defaultSemiBold">app-example</ThemedText>. 75 + </ThemedText> 76 + </ThemedView> 77 + </ParallaxScrollView> 78 + ); 79 + } 80 + 81 + const styles = StyleSheet.create({ 82 + titleContainer: { 83 + flexDirection: 'row', 84 + alignItems: 'center', 85 + gap: 8, 86 + }, 87 + stepContainer: { 88 + gap: 8, 89 + marginBottom: 8, 90 + }, 91 + reactLogo: { 92 + height: 178, 93 + width: 290, 94 + bottom: 0, 95 + left: 0, 96 + position: 'absolute', 97 + }, 98 + });
+24
app/_layout.tsx
··· 1 + import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 2 + import { Stack } from 'expo-router'; 3 + import { StatusBar } from 'expo-status-bar'; 4 + import 'react-native-reanimated'; 5 + 6 + import { useColorScheme } from '@/hooks/use-color-scheme'; 7 + 8 + export const unstable_settings = { 9 + anchor: '(tabs)', 10 + }; 11 + 12 + export default function RootLayout() { 13 + const colorScheme = useColorScheme(); 14 + 15 + return ( 16 + <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> 17 + <Stack> 18 + <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> 19 + <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} /> 20 + </Stack> 21 + <StatusBar style="auto" /> 22 + </ThemeProvider> 23 + ); 24 + }
+29
app/modal.tsx
··· 1 + import { Link } from 'expo-router'; 2 + import { StyleSheet } from 'react-native'; 3 + 4 + import { ThemedText } from '@/components/themed-text'; 5 + import { ThemedView } from '@/components/themed-view'; 6 + 7 + export default function ModalScreen() { 8 + return ( 9 + <ThemedView style={styles.container}> 10 + <ThemedText type="title">This is a modal</ThemedText> 11 + <Link href="/" dismissTo style={styles.link}> 12 + <ThemedText type="link">Go to home screen</ThemedText> 13 + </Link> 14 + </ThemedView> 15 + ); 16 + } 17 + 18 + const styles = StyleSheet.create({ 19 + container: { 20 + flex: 1, 21 + alignItems: 'center', 22 + justifyContent: 'center', 23 + padding: 20, 24 + }, 25 + link: { 26 + marginTop: 15, 27 + paddingVertical: 15, 28 + }, 29 + });
assets/images/android-icon-background.png

This is a binary file and will not be displayed.

assets/images/android-icon-foreground.png

This is a binary file and will not be displayed.

assets/images/android-icon-monochrome.png

This is a binary file and will not be displayed.

assets/images/favicon.png

This is a binary file and will not be displayed.

assets/images/icon.png

This is a binary file and will not be displayed.

assets/images/partial-react-logo.png

This is a binary file and will not be displayed.

assets/images/react-logo.png

This is a binary file and will not be displayed.

assets/images/react-logo@2x.png

This is a binary file and will not be displayed.

assets/images/react-logo@3x.png

This is a binary file and will not be displayed.

assets/images/splash-icon.png

This is a binary file and will not be displayed.

+25
components/external-link.tsx
··· 1 + import { Href, Link } from 'expo-router'; 2 + import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser'; 3 + import { type ComponentProps } from 'react'; 4 + 5 + type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string }; 6 + 7 + export function ExternalLink({ href, ...rest }: Props) { 8 + return ( 9 + <Link 10 + target="_blank" 11 + {...rest} 12 + href={href} 13 + onPress={async (event) => { 14 + if (process.env.EXPO_OS !== 'web') { 15 + // Prevent the default behavior of linking to the default browser on native. 16 + event.preventDefault(); 17 + // Open the link in an in-app browser. 18 + await openBrowserAsync(href, { 19 + presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, 20 + }); 21 + } 22 + }} 23 + /> 24 + ); 25 + }
+18
components/haptic-tab.tsx
··· 1 + import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; 2 + import { PlatformPressable } from '@react-navigation/elements'; 3 + import * as Haptics from 'expo-haptics'; 4 + 5 + export function HapticTab(props: BottomTabBarButtonProps) { 6 + return ( 7 + <PlatformPressable 8 + {...props} 9 + onPressIn={(ev) => { 10 + if (process.env.EXPO_OS === 'ios') { 11 + // Add a soft haptic feedback when pressing down on the tabs. 12 + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 13 + } 14 + props.onPressIn?.(ev); 15 + }} 16 + /> 17 + ); 18 + }
+19
components/hello-wave.tsx
··· 1 + import Animated from 'react-native-reanimated'; 2 + 3 + export function HelloWave() { 4 + return ( 5 + <Animated.Text 6 + style={{ 7 + fontSize: 28, 8 + lineHeight: 32, 9 + marginTop: -6, 10 + animationName: { 11 + '50%': { transform: [{ rotate: '25deg' }] }, 12 + }, 13 + animationIterationCount: 4, 14 + animationDuration: '300ms', 15 + }}> 16 + 👋 17 + </Animated.Text> 18 + ); 19 + }
+79
components/parallax-scroll-view.tsx
··· 1 + import type { PropsWithChildren, ReactElement } from 'react'; 2 + import { StyleSheet } from 'react-native'; 3 + import Animated, { 4 + interpolate, 5 + useAnimatedRef, 6 + useAnimatedStyle, 7 + useScrollOffset, 8 + } from 'react-native-reanimated'; 9 + 10 + import { ThemedView } from '@/components/themed-view'; 11 + import { useColorScheme } from '@/hooks/use-color-scheme'; 12 + import { useThemeColor } from '@/hooks/use-theme-color'; 13 + 14 + const HEADER_HEIGHT = 250; 15 + 16 + type Props = PropsWithChildren<{ 17 + headerImage: ReactElement; 18 + headerBackgroundColor: { dark: string; light: string }; 19 + }>; 20 + 21 + export default function ParallaxScrollView({ 22 + children, 23 + headerImage, 24 + headerBackgroundColor, 25 + }: Props) { 26 + const backgroundColor = useThemeColor({}, 'background'); 27 + const colorScheme = useColorScheme() ?? 'light'; 28 + const scrollRef = useAnimatedRef<Animated.ScrollView>(); 29 + const scrollOffset = useScrollOffset(scrollRef); 30 + const headerAnimatedStyle = useAnimatedStyle(() => { 31 + return { 32 + transform: [ 33 + { 34 + translateY: interpolate( 35 + scrollOffset.value, 36 + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], 37 + [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] 38 + ), 39 + }, 40 + { 41 + scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), 42 + }, 43 + ], 44 + }; 45 + }); 46 + 47 + return ( 48 + <Animated.ScrollView 49 + ref={scrollRef} 50 + style={{ backgroundColor, flex: 1 }} 51 + scrollEventThrottle={16}> 52 + <Animated.View 53 + style={[ 54 + styles.header, 55 + { backgroundColor: headerBackgroundColor[colorScheme] }, 56 + headerAnimatedStyle, 57 + ]}> 58 + {headerImage} 59 + </Animated.View> 60 + <ThemedView style={styles.content}>{children}</ThemedView> 61 + </Animated.ScrollView> 62 + ); 63 + } 64 + 65 + const styles = StyleSheet.create({ 66 + container: { 67 + flex: 1, 68 + }, 69 + header: { 70 + height: HEADER_HEIGHT, 71 + overflow: 'hidden', 72 + }, 73 + content: { 74 + flex: 1, 75 + padding: 32, 76 + gap: 16, 77 + overflow: 'hidden', 78 + }, 79 + });
+60
components/themed-text.tsx
··· 1 + import { StyleSheet, Text, type TextProps } from 'react-native'; 2 + 3 + import { useThemeColor } from '@/hooks/use-theme-color'; 4 + 5 + export type ThemedTextProps = TextProps & { 6 + lightColor?: string; 7 + darkColor?: string; 8 + type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; 9 + }; 10 + 11 + export function ThemedText({ 12 + style, 13 + lightColor, 14 + darkColor, 15 + type = 'default', 16 + ...rest 17 + }: ThemedTextProps) { 18 + const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 19 + 20 + return ( 21 + <Text 22 + style={[ 23 + { color }, 24 + type === 'default' ? styles.default : undefined, 25 + type === 'title' ? styles.title : undefined, 26 + type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, 27 + type === 'subtitle' ? styles.subtitle : undefined, 28 + type === 'link' ? styles.link : undefined, 29 + style, 30 + ]} 31 + {...rest} 32 + /> 33 + ); 34 + } 35 + 36 + const styles = StyleSheet.create({ 37 + default: { 38 + fontSize: 16, 39 + lineHeight: 24, 40 + }, 41 + defaultSemiBold: { 42 + fontSize: 16, 43 + lineHeight: 24, 44 + fontWeight: '600', 45 + }, 46 + title: { 47 + fontSize: 32, 48 + fontWeight: 'bold', 49 + lineHeight: 32, 50 + }, 51 + subtitle: { 52 + fontSize: 20, 53 + fontWeight: 'bold', 54 + }, 55 + link: { 56 + lineHeight: 30, 57 + fontSize: 16, 58 + color: '#0a7ea4', 59 + }, 60 + });
+14
components/themed-view.tsx
··· 1 + import { View, type ViewProps } from 'react-native'; 2 + 3 + import { useThemeColor } from '@/hooks/use-theme-color'; 4 + 5 + export type ThemedViewProps = ViewProps & { 6 + lightColor?: string; 7 + darkColor?: string; 8 + }; 9 + 10 + export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { 11 + const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 12 + 13 + return <View style={[{ backgroundColor }, style]} {...otherProps} />; 14 + }
+45
components/ui/collapsible.tsx
··· 1 + import { PropsWithChildren, useState } from 'react'; 2 + import { StyleSheet, TouchableOpacity } from 'react-native'; 3 + 4 + import { ThemedText } from '@/components/themed-text'; 5 + import { ThemedView } from '@/components/themed-view'; 6 + import { IconSymbol } from '@/components/ui/icon-symbol'; 7 + import { Colors } from '@/constants/theme'; 8 + import { useColorScheme } from '@/hooks/use-color-scheme'; 9 + 10 + export function Collapsible({ children, title }: PropsWithChildren & { title: string }) { 11 + const [isOpen, setIsOpen] = useState(false); 12 + const theme = useColorScheme() ?? 'light'; 13 + 14 + return ( 15 + <ThemedView> 16 + <TouchableOpacity 17 + style={styles.heading} 18 + onPress={() => setIsOpen((value) => !value)} 19 + activeOpacity={0.8}> 20 + <IconSymbol 21 + name="chevron.right" 22 + size={18} 23 + weight="medium" 24 + color={theme === 'light' ? Colors.light.icon : Colors.dark.icon} 25 + style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }} 26 + /> 27 + 28 + <ThemedText type="defaultSemiBold">{title}</ThemedText> 29 + </TouchableOpacity> 30 + {isOpen && <ThemedView style={styles.content}>{children}</ThemedView>} 31 + </ThemedView> 32 + ); 33 + } 34 + 35 + const styles = StyleSheet.create({ 36 + heading: { 37 + flexDirection: 'row', 38 + alignItems: 'center', 39 + gap: 6, 40 + }, 41 + content: { 42 + marginTop: 6, 43 + marginLeft: 24, 44 + }, 45 + });
+32
components/ui/icon-symbol.ios.tsx
··· 1 + import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; 2 + import { StyleProp, ViewStyle } from 'react-native'; 3 + 4 + export function IconSymbol({ 5 + name, 6 + size = 24, 7 + color, 8 + style, 9 + weight = 'regular', 10 + }: { 11 + name: SymbolViewProps['name']; 12 + size?: number; 13 + color: string; 14 + style?: StyleProp<ViewStyle>; 15 + weight?: SymbolWeight; 16 + }) { 17 + return ( 18 + <SymbolView 19 + weight={weight} 20 + tintColor={color} 21 + resizeMode="scaleAspectFit" 22 + name={name} 23 + style={[ 24 + { 25 + width: size, 26 + height: size, 27 + }, 28 + style, 29 + ]} 30 + /> 31 + ); 32 + }
+41
components/ui/icon-symbol.tsx
··· 1 + // Fallback for using MaterialIcons on Android and web. 2 + 3 + import MaterialIcons from '@expo/vector-icons/MaterialIcons'; 4 + import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; 5 + import { ComponentProps } from 'react'; 6 + import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; 7 + 8 + type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>; 9 + type IconSymbolName = keyof typeof MAPPING; 10 + 11 + /** 12 + * Add your SF Symbols to Material Icons mappings here. 13 + * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). 14 + * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. 15 + */ 16 + const MAPPING = { 17 + 'house.fill': 'home', 18 + 'paperplane.fill': 'send', 19 + 'chevron.left.forwardslash.chevron.right': 'code', 20 + 'chevron.right': 'chevron-right', 21 + } as IconMapping; 22 + 23 + /** 24 + * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. 25 + * This ensures a consistent look across platforms, and optimal resource usage. 26 + * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. 27 + */ 28 + export function IconSymbol({ 29 + name, 30 + size = 24, 31 + color, 32 + style, 33 + }: { 34 + name: IconSymbolName; 35 + size?: number; 36 + color: string | OpaqueColorValue; 37 + style?: StyleProp<TextStyle>; 38 + weight?: SymbolWeight; 39 + }) { 40 + return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />; 41 + }
+53
constants/theme.ts
··· 1 + /** 2 + * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 + * 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. 4 + */ 5 + 6 + import { Platform } from 'react-native'; 7 + 8 + const tintColorLight = '#0a7ea4'; 9 + const tintColorDark = '#fff'; 10 + 11 + export const Colors = { 12 + light: { 13 + text: '#11181C', 14 + background: '#fff', 15 + tint: tintColorLight, 16 + icon: '#687076', 17 + tabIconDefault: '#687076', 18 + tabIconSelected: tintColorLight, 19 + }, 20 + dark: { 21 + text: '#ECEDEE', 22 + background: '#151718', 23 + tint: tintColorDark, 24 + icon: '#9BA1A6', 25 + tabIconDefault: '#9BA1A6', 26 + tabIconSelected: tintColorDark, 27 + }, 28 + }; 29 + 30 + export const Fonts = Platform.select({ 31 + ios: { 32 + /** iOS `UIFontDescriptorSystemDesignDefault` */ 33 + sans: 'system-ui', 34 + /** iOS `UIFontDescriptorSystemDesignSerif` */ 35 + serif: 'ui-serif', 36 + /** iOS `UIFontDescriptorSystemDesignRounded` */ 37 + rounded: 'ui-rounded', 38 + /** iOS `UIFontDescriptorSystemDesignMonospaced` */ 39 + mono: 'ui-monospace', 40 + }, 41 + default: { 42 + sans: 'normal', 43 + serif: 'serif', 44 + rounded: 'normal', 45 + mono: 'monospace', 46 + }, 47 + web: { 48 + sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", 49 + serif: "Georgia, 'Times New Roman', serif", 50 + rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", 51 + mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", 52 + }, 53 + });
+10
eslint.config.js
··· 1 + // https://docs.expo.dev/guides/using-eslint/ 2 + const { defineConfig } = require('eslint/config'); 3 + const expoConfig = require('eslint-config-expo/flat'); 4 + 5 + module.exports = defineConfig([ 6 + expoConfig, 7 + { 8 + ignores: ['dist/*'], 9 + }, 10 + ]);
+1
hooks/use-color-scheme.ts
··· 1 + export { useColorScheme } from 'react-native';
+21
hooks/use-color-scheme.web.ts
··· 1 + import { useEffect, useState } from 'react'; 2 + import { useColorScheme as useRNColorScheme } from 'react-native'; 3 + 4 + /** 5 + * To support static rendering, this value needs to be re-calculated on the client side for web 6 + */ 7 + export function useColorScheme() { 8 + const [hasHydrated, setHasHydrated] = useState(false); 9 + 10 + useEffect(() => { 11 + setHasHydrated(true); 12 + }, []); 13 + 14 + const colorScheme = useRNColorScheme(); 15 + 16 + if (hasHydrated) { 17 + return colorScheme; 18 + } 19 + 20 + return 'light'; 21 + }
+21
hooks/use-theme-color.ts
··· 1 + /** 2 + * Learn more about light and dark modes: 3 + * https://docs.expo.dev/guides/color-schemes/ 4 + */ 5 + 6 + import { Colors } from '@/constants/theme'; 7 + import { useColorScheme } from '@/hooks/use-color-scheme'; 8 + 9 + export function useThemeColor( 10 + props: { light?: string; dark?: string }, 11 + colorName: keyof typeof Colors.light & keyof typeof Colors.dark 12 + ) { 13 + const theme = useColorScheme() ?? 'light'; 14 + const colorFromProps = props[theme]; 15 + 16 + if (colorFromProps) { 17 + return colorFromProps; 18 + } else { 19 + return Colors[theme][colorName]; 20 + } 21 + }
+47
package.json
··· 1 + { 2 + "name": "rabbit", 3 + "main": "expo-router/entry", 4 + "version": "1.0.0", 5 + "scripts": { 6 + "start": "expo start", 7 + "reset-project": "node ./scripts/reset-project.js", 8 + "android": "expo start --android", 9 + "ios": "expo start --ios", 10 + "web": "expo start --web", 11 + "lint": "expo lint" 12 + }, 13 + "dependencies": { 14 + "@expo/vector-icons": "^15.0.2", 15 + "@react-navigation/bottom-tabs": "^7.4.0", 16 + "@react-navigation/elements": "^2.6.3", 17 + "@react-navigation/native": "^7.1.8", 18 + "expo": "~54.0.13", 19 + "expo-constants": "~18.0.9", 20 + "expo-font": "~14.0.9", 21 + "expo-haptics": "~15.0.7", 22 + "expo-image": "~3.0.9", 23 + "expo-linking": "~8.0.8", 24 + "expo-router": "~6.0.11", 25 + "expo-splash-screen": "~31.0.10", 26 + "expo-status-bar": "~3.0.8", 27 + "expo-symbols": "~1.0.7", 28 + "expo-system-ui": "~6.0.7", 29 + "expo-web-browser": "~15.0.8", 30 + "react": "19.1.0", 31 + "react-dom": "19.1.0", 32 + "react-native": "0.81.4", 33 + "react-native-gesture-handler": "~2.28.0", 34 + "react-native-worklets": "0.5.1", 35 + "react-native-reanimated": "~4.1.1", 36 + "react-native-safe-area-context": "~5.6.0", 37 + "react-native-screens": "~4.16.0", 38 + "react-native-web": "~0.21.0" 39 + }, 40 + "devDependencies": { 41 + "@types/react": "~19.1.0", 42 + "typescript": "~5.9.2", 43 + "eslint": "^9.25.0", 44 + "eslint-config-expo": "~10.0.0" 45 + }, 46 + "private": true 47 + }
+112
scripts/reset-project.js
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * This script is used to reset the project to a blank state. 5 + * 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. 6 + * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 + */ 8 + 9 + const fs = require("fs"); 10 + const path = require("path"); 11 + const readline = require("readline"); 12 + 13 + const root = process.cwd(); 14 + const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; 15 + const exampleDir = "app-example"; 16 + const newAppDir = "app"; 17 + const exampleDirPath = path.join(root, exampleDir); 18 + 19 + const indexContent = `import { Text, View } from "react-native"; 20 + 21 + export default function Index() { 22 + return ( 23 + <View 24 + style={{ 25 + flex: 1, 26 + justifyContent: "center", 27 + alignItems: "center", 28 + }} 29 + > 30 + <Text>Edit app/index.tsx to edit this screen.</Text> 31 + </View> 32 + ); 33 + } 34 + `; 35 + 36 + const layoutContent = `import { Stack } from "expo-router"; 37 + 38 + export default function RootLayout() { 39 + return <Stack />; 40 + } 41 + `; 42 + 43 + const rl = readline.createInterface({ 44 + input: process.stdin, 45 + output: process.stdout, 46 + }); 47 + 48 + const moveDirectories = async (userInput) => { 49 + try { 50 + if (userInput === "y") { 51 + // Create the app-example directory 52 + await fs.promises.mkdir(exampleDirPath, { recursive: true }); 53 + console.log(`📁 /${exampleDir} directory created.`); 54 + } 55 + 56 + // Move old directories to new app-example directory or delete them 57 + for (const dir of oldDirs) { 58 + const oldDirPath = path.join(root, dir); 59 + if (fs.existsSync(oldDirPath)) { 60 + if (userInput === "y") { 61 + const newDirPath = path.join(root, exampleDir, dir); 62 + await fs.promises.rename(oldDirPath, newDirPath); 63 + console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); 64 + } else { 65 + await fs.promises.rm(oldDirPath, { recursive: true, force: true }); 66 + console.log(`❌ /${dir} deleted.`); 67 + } 68 + } else { 69 + console.log(`➡️ /${dir} does not exist, skipping.`); 70 + } 71 + } 72 + 73 + // Create new /app directory 74 + const newAppDirPath = path.join(root, newAppDir); 75 + await fs.promises.mkdir(newAppDirPath, { recursive: true }); 76 + console.log("\n📁 New /app directory created."); 77 + 78 + // Create index.tsx 79 + const indexPath = path.join(newAppDirPath, "index.tsx"); 80 + await fs.promises.writeFile(indexPath, indexContent); 81 + console.log("📄 app/index.tsx created."); 82 + 83 + // Create _layout.tsx 84 + const layoutPath = path.join(newAppDirPath, "_layout.tsx"); 85 + await fs.promises.writeFile(layoutPath, layoutContent); 86 + console.log("📄 app/_layout.tsx created."); 87 + 88 + console.log("\n✅ Project reset complete. Next steps:"); 89 + console.log( 90 + `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ 91 + userInput === "y" 92 + ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` 93 + : "" 94 + }` 95 + ); 96 + } catch (error) { 97 + console.error(`❌ Error during script execution: ${error.message}`); 98 + } 99 + }; 100 + 101 + rl.question( 102 + "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", 103 + (answer) => { 104 + const userInput = answer.trim().toLowerCase() || "y"; 105 + if (userInput === "y" || userInput === "n") { 106 + moveDirectories(userInput).finally(() => rl.close()); 107 + } else { 108 + console.log("❌ Invalid input. Please enter 'Y' or 'N'."); 109 + rl.close(); 110 + } 111 + } 112 + );
+17
tsconfig.json
··· 1 + { 2 + "extends": "expo/tsconfig.base", 3 + "compilerOptions": { 4 + "strict": true, 5 + "paths": { 6 + "@/*": [ 7 + "./*" 8 + ] 9 + } 10 + }, 11 + "include": [ 12 + "**/*.ts", 13 + "**/*.tsx", 14 + ".expo/types/**/*.ts", 15 + "expo-env.d.ts" 16 + ] 17 + }