├── 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 |
168 |
169 |
170 |
175 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
193 |
198 |
199 |
204 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | );
219 | };
220 |
221 | const Header = ({ progress }: { progress: SharedValue }) => {
222 | const { top } = useSafeAreaInsets();
223 | const card = useThemeColor("card");
224 |
225 | const animatedStyle = useAnimatedStyle(() => {
226 | return {
227 | height: interpolate(
228 | progress.value,
229 | [0, 1],
230 | [HEADER_HEIGHT, HEADER_HEIGHT + top / 2],
231 | Extrapolation.CLAMP
232 | ),
233 | paddingTop: interpolate(
234 | progress.value,
235 | [0, 1],
236 | [0, top],
237 | Extrapolation.CLAMP
238 | ),
239 | };
240 | });
241 |
242 | return (
243 |
252 |
253 |
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 |
--------------------------------------------------------------------------------