├── hooks ├── use-color-scheme.ts ├── use-color-scheme.web.ts └── use-theme-color.ts ├── constants ├── index.ts └── theme.ts ├── assets └── images │ ├── icon.png │ ├── favicon.png │ ├── react-logo.png │ ├── splash-icon.png │ ├── adaptive-icon.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── partial-react-logo.png ├── eslint.config.js ├── app ├── (tabs) │ ├── index.tsx │ ├── browse.tsx │ ├── shared.tsx │ └── _layout.tsx └── _layout.tsx ├── tsconfig.json ├── functions └── index.ts ├── .gitignore ├── context └── shared-context.tsx ├── components ├── themed-view.tsx ├── ui │ └── Button.tsx ├── themed-text.tsx ├── empty-state.tsx └── preview-base.tsx ├── app.json ├── package.json └── README.md /hooks/use-color-scheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /constants/index.ts: -------------------------------------------------------------------------------- 1 | export const HEADER_HEIGHT = 72; 2 | export const BOTTOM_INSET = 92; 3 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/splash-icon.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/expo-ios-preview/HEAD/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /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 | ]); 11 | -------------------------------------------------------------------------------- /app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import PreviewBase from "@/components/preview-base"; 2 | import EmptyState from "@/components/empty-state"; 3 | 4 | export default function Tab() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/(tabs)/browse.tsx: -------------------------------------------------------------------------------- 1 | import PreviewBase from "@/components/preview-base"; 2 | import EmptyState from "@/components/empty-state"; 3 | 4 | export default function Tab() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/(tabs)/shared.tsx: -------------------------------------------------------------------------------- 1 | import PreviewBase from "@/components/preview-base"; 2 | import EmptyState from "@/components/empty-state"; 3 | 4 | export default function Tab() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /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 | } 18 | -------------------------------------------------------------------------------- /functions/index.ts: -------------------------------------------------------------------------------- 1 | export const hex2Rgb = (hex: string) => 2 | hex.match(/\w\w/g)?.map((x) => parseInt(x, 16)) || []; 3 | 4 | export const hexToRgb = (hex: string) => { 5 | const [r, g, b] = hex2Rgb(hex.replace("#", "")); 6 | return `rgb(${r}, ${g}, ${b})`; 7 | }; 8 | 9 | export const hexToRgba = (hex: string, alpha: number) => { 10 | const [r, g, b] = hex2Rgb(hex.replace("#", "")); 11 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 12 | }; 13 | -------------------------------------------------------------------------------- /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 | } 22 | -------------------------------------------------------------------------------- /.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 | /ios 12 | /android 13 | 14 | # Native 15 | .kotlin/ 16 | *.orig.* 17 | *.jks 18 | *.p8 19 | *.p12 20 | *.key 21 | *.mobileprovision 22 | 23 | # Metro 24 | .metro-health-check* 25 | 26 | # debug 27 | npm-debug.* 28 | yarn-debug.* 29 | yarn-error.* 30 | 31 | # macOS 32 | .DS_Store 33 | *.pem 34 | 35 | # local env files 36 | .env*.local 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | 41 | app-example 42 | -------------------------------------------------------------------------------- /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 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark, 11 | props?: { light?: string; dark?: string } 12 | ) { 13 | const theme = useColorScheme() ?? "light"; 14 | const colorFromProps = props && props[theme]; 15 | 16 | if (colorFromProps) { 17 | return colorFromProps; 18 | } else { 19 | return Colors[theme][colorName]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs"; 2 | 3 | export default function TabLayout() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /context/shared-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { SharedValue, useSharedValue } from "react-native-reanimated"; 3 | 4 | interface SharedContextProps { 5 | fullscreen: SharedValue; 6 | progress: SharedValue; 7 | } 8 | 9 | const SharedContext = createContext(null); 10 | 11 | export const SharedContextProvider = ({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) => { 16 | const fullscreen = useSharedValue(false); 17 | const progress = useSharedValue(0); 18 | 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export const useSharedContext = () => { 27 | const context = useContext(SharedContext); 28 | if (!context) { 29 | throw new Error( 30 | "useSharedContext must be used within a SharedContextProvider" 31 | ); 32 | } 33 | return context; 34 | }; 35 | -------------------------------------------------------------------------------- /components/themed-view.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from "react-native"; 2 | 3 | import { useThemeColor } from "@/hooks/use-theme-color"; 4 | import { Colors } from "@/constants/theme"; 5 | import { cloneElement } from "react"; 6 | 7 | export type ThemedViewProps = ViewProps & { 8 | colorName?: keyof typeof Colors.light & keyof typeof Colors.dark; 9 | }; 10 | 11 | export function ThemedView({ 12 | style, 13 | colorName = "background", 14 | ...otherProps 15 | }: ThemedViewProps) { 16 | const backgroundColor = useThemeColor(colorName); 17 | 18 | return ; 19 | } 20 | 21 | export function ThemedViewWrapper({ 22 | children, 23 | colorName = "background", 24 | style, 25 | ...rest 26 | }: ThemedViewProps & { children: React.ReactElement }) { 27 | const backgroundColor = useThemeColor(colorName); 28 | 29 | const combinedStyle = [{ backgroundColor }, style]; 30 | 31 | return cloneElement(children, { 32 | style: [(children.props as any).style ?? {}, ...combinedStyle], 33 | ...rest, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme, 3 | DefaultTheme, 4 | ThemeProvider, 5 | } from "@react-navigation/native"; 6 | import { Stack } from "expo-router"; 7 | import { StatusBar } from "expo-status-bar"; 8 | import "react-native-reanimated"; 9 | 10 | import { useColorScheme } from "@/hooks/use-color-scheme"; 11 | import { SharedContextProvider } from "@/context/shared-context"; 12 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 13 | 14 | export const unstable_settings = { 15 | anchor: "(tabs)", 16 | }; 17 | 18 | export default function RootLayout() { 19 | const colorScheme = useColorScheme(); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ios-preview-app", 4 | "slug": "ios-preview-app", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "iospreviewapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true, 13 | "bundleIdentifier": "com.solarin.iospreviewapp" 14 | }, 15 | "android": { 16 | "adaptiveIcon": { 17 | "foregroundImage": "./assets/images/adaptive-icon.png", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "edgeToEdgeEnabled": true, 21 | "predictiveBackGestureEnabled": true, 22 | "package": "com.solarin.iospreviewapp" 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 | } 38 | ] 39 | ], 40 | "experiments": { 41 | "typedRoutes": true, 42 | "reactCompiler": true 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | TouchableOpacity, 3 | StyleSheet, 4 | ViewStyle, 5 | StyleProp, 6 | } from "react-native"; 7 | import React, { ReactElement } from "react"; 8 | import { useThemeColor } from "@/hooks/use-theme-color"; 9 | import { ThemedText, ThemedTextWrapper } from "../themed-text"; 10 | 11 | interface ButtonProps { 12 | title?: string; 13 | children?: ReactElement; 14 | style?: StyleProp; 15 | } 16 | 17 | export default function Button({ title, children, style }: ButtonProps) { 18 | const text = useThemeColor("text"); 19 | const card = useThemeColor("card"); 20 | 21 | return ( 22 | 33 | {children ? ( 34 | {children} 35 | ) : ( 36 | 40 | {title} 41 | 42 | )} 43 | 44 | ); 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | button: { 49 | width: "100%", 50 | padding: 16, 51 | borderRadius: 100, 52 | borderWidth: 1, 53 | }, 54 | text: { 55 | textAlign: "center", 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ios-preview-app", 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 run:android", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "lint": "expo lint" 12 | }, 13 | "dependencies": { 14 | "@expo/vector-icons": "^14.1.0", 15 | "@gorhom/bottom-sheet": "^5.2.3", 16 | "@react-navigation/bottom-tabs": "^7.4.0", 17 | "@react-navigation/elements": "^2.6.3", 18 | "@react-navigation/native": "^7.1.8", 19 | "expo": "~54.0.0-preview.4", 20 | "expo-blur": "~15.0.2", 21 | "expo-constants": "~18.0.2", 22 | "expo-dev-client": "~6.0.3", 23 | "expo-font": "~14.0.2", 24 | "expo-haptics": "~15.0.2", 25 | "expo-image": "~3.0.2", 26 | "expo-linking": "~8.0.2", 27 | "expo-router": "~6.0.0-beta.4", 28 | "expo-splash-screen": "~31.0.3", 29 | "expo-status-bar": "~3.0.3", 30 | "expo-symbols": "~1.0.2", 31 | "expo-web-browser": "~15.0.2", 32 | "react": "19.1.0", 33 | "react-dom": "19.1.0", 34 | "react-native": "0.81.0", 35 | "react-native-gesture-handler": "~2.28.0", 36 | "react-native-reanimated": "~4.0.2", 37 | "react-native-safe-area-context": "~5.6.0", 38 | "react-native-screens": "~4.14.0", 39 | "react-native-web": "~0.21.0", 40 | "react-native-worklets": "~0.4.1" 41 | }, 42 | "devDependencies": { 43 | "@types/react": "~19.1.0", 44 | "eslint": "^9.25.0", 45 | "eslint-config-expo": "~10.0.0", 46 | "typescript": "~5.9.2" 47 | }, 48 | "private": true 49 | } 50 | -------------------------------------------------------------------------------- /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 | export const Colors = { 9 | light: { 10 | text: "#13181C", 11 | background: "#F5F5F5", 12 | card: "#FFFFFF", 13 | tray: "#FFFFFF", 14 | baseTray: "#FFFFFF", 15 | }, 16 | dark: { 17 | text: "#FFFFFF", 18 | background: "#151517", 19 | card: "#1C1C1E", 20 | tray: "#2C2C2E", 21 | baseTray: "#232324", 22 | }, 23 | }; 24 | 25 | export const Fonts = Platform.select({ 26 | ios: { 27 | /** iOS `UIFontDescriptorSystemDesignDefault` */ 28 | sans: "system-ui", 29 | /** iOS `UIFontDescriptorSystemDesignSerif` */ 30 | serif: "ui-serif", 31 | /** iOS `UIFontDescriptorSystemDesignRounded` */ 32 | rounded: "ui-rounded", 33 | /** iOS `UIFontDescriptorSystemDesignMonospaced` */ 34 | mono: "ui-monospace", 35 | }, 36 | default: { 37 | sans: "normal", 38 | serif: "serif", 39 | rounded: "normal", 40 | mono: "monospace", 41 | }, 42 | web: { 43 | sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", 44 | serif: "Georgia, 'Times New Roman', serif", 45 | rounded: 46 | "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", 47 | mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /components/themed-text.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, type TextProps } from "react-native"; 2 | 3 | import { useThemeColor } from "@/hooks/use-theme-color"; 4 | import { Colors } from "@/constants/theme"; 5 | import { cloneElement, ReactElement } from "react"; 6 | 7 | export type ThemedTextProps = TextProps & { 8 | type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link" | "bold"; 9 | colorName?: keyof typeof Colors.light & keyof typeof Colors.dark; 10 | }; 11 | 12 | export function ThemedText({ 13 | style, 14 | type = "default", 15 | colorName = "text", 16 | ...rest 17 | }: ThemedTextProps) { 18 | const color = useThemeColor(colorName); 19 | const variantKey = type as keyof typeof styles; 20 | 21 | return ; 22 | } 23 | 24 | export function ThemedTextWrapper({ 25 | children, 26 | type = "default", 27 | colorName = "text", 28 | style, 29 | ignoreStyle = true, 30 | ...rest 31 | }: ThemedTextProps & { children: ReactElement; ignoreStyle?: boolean }) { 32 | const color = useThemeColor(colorName); 33 | const variantKey = type as keyof typeof styles; 34 | 35 | const combinedStyle = [{ color }, !ignoreStyle && styles[variantKey], style]; 36 | 37 | return cloneElement(children, { 38 | style: [(children.props as any).style ?? {}, ...combinedStyle], 39 | ...rest, 40 | }); 41 | } 42 | 43 | const styles = StyleSheet.create({ 44 | default: { 45 | fontSize: 16, 46 | }, 47 | defaultSemiBold: { 48 | fontSize: 17, 49 | fontWeight: "500", 50 | }, 51 | title: { 52 | fontSize: 32, 53 | fontWeight: "600", 54 | }, 55 | subtitle: { 56 | fontSize: 15, 57 | }, 58 | link: { 59 | lineHeight: 30, 60 | fontSize: 16, 61 | color: "#0a7ea4", 62 | }, 63 | bold: { 64 | fontSize: 16, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /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. 51 | -------------------------------------------------------------------------------- /components/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemedText } from "./themed-text"; 3 | import Animated, { 4 | interpolate, 5 | useAnimatedStyle, 6 | useSharedValue, 7 | } from "react-native-reanimated"; 8 | import { useSharedContext } from "@/context/shared-context"; 9 | import { StyleSheet, View } from "react-native"; 10 | import { BOTTOM_INSET } from "@/constants"; 11 | import { SymbolView, SymbolViewProps } from "expo-symbols"; 12 | import { useThemeColor } from "@/hooks/use-theme-color"; 13 | import { hexToRgba } from "@/functions"; 14 | 15 | interface EmptyStateProps { 16 | title?: string; 17 | symbol?: SymbolViewProps["name"]; 18 | } 19 | 20 | export default function EmptyState({ 21 | title, 22 | symbol = "doc.fill", 23 | }: EmptyStateProps) { 24 | const displayedTitle = `No${title ? ` ${title}` : ""} Files`; 25 | const displayedSubtitle = `${title ? title : ""} files will appear here.`; 26 | 27 | const text = useThemeColor("text"); 28 | const tintColor = hexToRgba(text, 0.65); 29 | 30 | const { progress } = useSharedContext(); 31 | const height = useSharedValue(0); 32 | 33 | const animatedStyle = useAnimatedStyle(() => { 34 | const h = height.value; 35 | return { 36 | height: interpolate(progress.value, [0, 1], [h / 2 - BOTTOM_INSET, h]), 37 | }; 38 | }); 39 | 40 | const onLayout = (event: any) => { 41 | const { height: newHeight } = event.nativeEvent.layout; 42 | height.value = newHeight; 43 | }; 44 | 45 | return ( 46 | 47 | 48 | 54 | 55 | {displayedTitle} 56 | 57 | {displayedSubtitle} 58 | 59 | 60 | ); 61 | } 62 | 63 | const styles = StyleSheet.create({ 64 | container: { 65 | justifyContent: "center", 66 | alignItems: "center", 67 | }, 68 | title: { 69 | fontSize: 25, 70 | lineHeight: 36, 71 | }, 72 | subtitle: { 73 | fontSize: 16, 74 | lineHeight: 24, 75 | opacity: 0.65, 76 | }, 77 | symbol: { 78 | width: 64, 79 | height: 64, 80 | margin: 8, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /components/preview-base.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, StyleSheet, useWindowDimensions, View } from "react-native"; 2 | import BottomSheet, { BottomSheetScrollView } from "@gorhom/bottom-sheet"; 3 | import { useCallback, useRef } from "react"; 4 | import { useThemeColor } from "@/hooks/use-theme-color"; 5 | import { ThemedView } from "./themed-view"; 6 | import { BlurView } from "expo-blur"; 7 | import { 8 | SafeAreaView, 9 | useSafeAreaInsets, 10 | } from "react-native-safe-area-context"; 11 | import Animated, { 12 | Extrapolation, 13 | interpolate, 14 | SharedValue, 15 | useAnimatedReaction, 16 | useAnimatedStyle, 17 | useDerivedValue, 18 | useSharedValue, 19 | } from "react-native-reanimated"; 20 | import { ThemedText } from "./themed-text"; 21 | import Button from "./ui/Button"; 22 | import { useSharedContext } from "@/context/shared-context"; 23 | import { Link, useFocusEffect } from "expo-router"; 24 | import Ionicons from "@expo/vector-icons/Ionicons"; 25 | import { hexToRgba } from "@/functions"; 26 | import { BOTTOM_INSET, HEADER_HEIGHT } from "@/constants"; 27 | 28 | export const SPRING_CONFIG = { 29 | damping: 26, 30 | stiffness: 200, 31 | mass: 0.7, 32 | }; 33 | 34 | const AnimatedBlurView = Animated.createAnimatedComponent(BlurView); 35 | const AnimatedBottomSheetScrollView = Animated.createAnimatedComponent( 36 | BottomSheetScrollView 37 | ); 38 | 39 | const PARRALAX_FACTOR = 150; 40 | const MIN_INTENSITY = 42; 41 | const MAX_INTENSITY = 100; 42 | 43 | export default function PreviewBase({ 44 | children, 45 | }: { 46 | children: React.ReactNode; 47 | }) { 48 | const { fullscreen, progress: _progress } = useSharedContext(); 49 | const { top } = useSafeAreaInsets(); 50 | const bottomSheetRef = useRef(null); 51 | const card = useThemeColor("card"); 52 | const animatedPosition = useSharedValue(0); 53 | const { height } = useWindowDimensions(); 54 | const cardBg = hexToRgba(card, 0.4); 55 | const intensity = useSharedValue(24); 56 | const animatedProgress = useDerivedValue(() => { 57 | const progress = 1 - animatedPosition.value / height / 0.5; 58 | return Math.min(progress, 1); 59 | }); 60 | 61 | const handleSheetChanges = useCallback((index: number) => {}, []); 62 | 63 | const toggleBottomSheet = useCallback(() => { 64 | if (fullscreen.value) { 65 | bottomSheetRef.current?.expand({ duration: 0 }); 66 | } else { 67 | bottomSheetRef.current?.collapse({ duration: 0 }); 68 | } 69 | }, [fullscreen]); 70 | 71 | useFocusEffect( 72 | useCallback(() => { 73 | toggleBottomSheet(); 74 | }, [toggleBottomSheet]) 75 | ); 76 | 77 | useAnimatedReaction( 78 | () => animatedProgress.value, 79 | (progress) => { 80 | fullscreen.value = progress > 0.5; 81 | _progress.value = progress; 82 | 83 | intensity.value = interpolate( 84 | progress, 85 | [0, 1], 86 | [MIN_INTENSITY, MAX_INTENSITY], 87 | Extrapolation.CLAMP 88 | ); 89 | } 90 | ); 91 | 92 | const scrollAnimatedStyle = useAnimatedStyle(() => { 93 | return { 94 | paddingTop: interpolate( 95 | _progress.value, 96 | [0, 1], 97 | [HEADER_HEIGHT, HEADER_HEIGHT + top / 1.5], 98 | Extrapolation.CLAMP 99 | ), 100 | }; 101 | }); 102 | 103 | return ( 104 | 105 | 106 | null} 112 | snapPoints={["50%", "100%"]} 113 | overDragResistanceFactor={3} 114 | animationConfigs={SPRING_CONFIG} 115 | topInset={-1} 116 | enableDynamicSizing={false} 117 | backgroundComponent={({ style }) => ( 118 | 119 | )} 120 | > 121 |
122 | 127 | {children} 128 | 129 | 130 | 131 | ); 132 | } 133 | 134 | const PreviewTray = ({ progress }: { progress: SharedValue }) => { 135 | const text = useThemeColor("text"); 136 | 137 | const animatedStyle = useAnimatedStyle(() => { 138 | return { 139 | transform: [ 140 | { 141 | translateY: -progress.value * PARRALAX_FACTOR, 142 | }, 143 | ], 144 | }; 145 | }); 146 | 147 | const handleMenuActionPress = useCallback(() => { 148 | Alert.alert("Menu action pressed"); 149 | }, []); 150 | 151 | return ( 152 | 153 | 154 | 155 | 159 | 160 | 161 | 162 | Preview 163 | 164 | 165 | 166 | 167 | 256 | 259 | 260 | 261 | ); 262 | }; 263 | 264 | const styles = StyleSheet.create({ 265 | container: { 266 | flex: 1, 267 | }, 268 | contentContainer: { 269 | flexGrow: 1, 270 | paddingBottom: BOTTOM_INSET, 271 | }, 272 | bgStyle: { 273 | width: "100.4%", 274 | marginLeft: "-0.2%", 275 | borderRadius: 36, 276 | borderCurve: "continuous", 277 | overflow: "hidden", 278 | borderWidth: 1, 279 | borderColor: "rgba(255, 255, 255, 0.05)", 280 | boxShadow: "0px 0px 24px rgba(0, 0, 0, 0.08)", 281 | }, 282 | trayContainer: { 283 | flex: 1, 284 | padding: 12, 285 | paddingBottom: 0, 286 | }, 287 | baseTray: { 288 | flex: 1, 289 | borderRadius: 36, 290 | opacity: 0.8, 291 | borderCurve: "continuous", 292 | marginTop: 54, 293 | borderWidth: 1, 294 | boxShadow: "0px 0px 12px rgba(0, 0, 0, 0.05)", 295 | }, 296 | floatingTray: { 297 | position: "absolute", 298 | top: 0, 299 | right: 0, 300 | left: 0, 301 | bottom: 0, 302 | margin: 24, 303 | marginTop: 16, 304 | borderRadius: 24, 305 | borderCurve: "continuous", 306 | boxShadow: "0px 0px 54px rgba(0, 0, 0, 0.08)", 307 | }, 308 | innerTray: { 309 | flex: 0.5, 310 | padding: 24, 311 | paddingBottom: 48, 312 | alignItems: "center", 313 | justifyContent: "center", 314 | gap: 32, 315 | }, 316 | title: { 317 | fontSize: 62, 318 | }, 319 | buttonContainer: { 320 | width: "100%", 321 | gap: 8, 322 | }, 323 | headerWrapper: { 324 | position: "absolute", 325 | borderTopRightRadius: 36, 326 | borderTopLeftRadius: 36, 327 | width: "100%", 328 | height: HEADER_HEIGHT, 329 | top: 0, 330 | zIndex: 1000, 331 | overflow: "hidden", 332 | justifyContent: "center", 333 | }, 334 | header: { 335 | padding: 16, 336 | gap: 12, 337 | flexDirection: "row", 338 | alignItems: "center", 339 | justifyContent: "flex-end", 340 | width: "100%", 341 | }, 342 | headerBtn: { 343 | width: 40, 344 | padding: 0, 345 | alignItems: "center", 346 | justifyContent: "center", 347 | aspectRatio: 1, 348 | }, 349 | }); 350 | --------------------------------------------------------------------------------