diff --git a/App.tsx b/App.tsx index 62bc986..c0976c0 100644 --- a/App.tsx +++ b/App.tsx @@ -5,31 +5,21 @@ */ import {ApolloProvider} from '@apollo/client'; -import {useEffect, useState} from 'react'; +import {NavigationContainer} from '@react-navigation/native'; +import {useEffect} from 'react'; import { ActivityIndicator, - Pressable, StatusBar, StyleSheet, - Text, useColorScheme, View, } from 'react-native'; -import { - SafeAreaProvider, - useSafeAreaInsets, -} from 'react-native-safe-area-context'; -import {apolloClient, logout} from './src/auth/authClient'; +import {SafeAreaProvider} from 'react-native-safe-area-context'; +import {apolloClient} from './src/auth/authClient'; import {useDeepLinkListener} from './src/auth/deepLinks'; import {LoginScreen} from './src/auth/LoginScreen'; -import { - type AuthUser, - tokenStore, - useAuth, - useAuthHydrated, -} from './src/auth/tokenStore'; -import {MapScreen} from './src/map/MapScreen'; -import {Sheet} from './src/ui/Sheet'; +import {tokenStore, useAuth, useAuthHydrated} from './src/auth/tokenStore'; +import {RootNavigator} from './src/navigation/RootNavigator'; function App() { const isDarkMode = useColorScheme() === 'dark'; @@ -67,53 +57,9 @@ function AppContent() { } return ( - - - - - ); -} - -function AccountMenu({user}: {user: AuthUser}) { - const safeAreaInsets = useSafeAreaInsets(); - const [open, setOpen] = useState(false); - - const initial = user.email.trim().charAt(0).toUpperCase() || '?'; - - return ( - <> - setOpen(true)} - style={({pressed}) => [ - styles.avatarButton, - {top: safeAreaInsets.top + 12}, - pressed && styles.avatarPressed, - ]}> - {initial} - - setOpen(false)} - scrimAccessibilityLabel="Close account menu"> - - {user.email} - - { - setOpen(false); - logout(); - }} - style={({pressed}) => [ - styles.sheetItem, - pressed && styles.sheetItemPressed, - ]}> - Log out - - - + + + ); } @@ -125,45 +71,6 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - avatarButton: { - position: 'absolute', - right: 16, - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: '#1d6fe0', - alignItems: 'center', - justifyContent: 'center', - shadowColor: '#000', - shadowOpacity: 0.2, - shadowRadius: 4, - shadowOffset: {width: 0, height: 2}, - elevation: 4, - }, - avatarPressed: {opacity: 0.8}, - avatarText: { - color: '#ffffff', - fontSize: 16, - fontWeight: '600', - }, - sheetEmail: { - fontSize: 12, - color: '#666', - paddingHorizontal: 12, - paddingBottom: 8, - }, - sheetItem: { - paddingHorizontal: 12, - paddingVertical: 14, - borderRadius: 8, - }, - sheetItemPressed: { - backgroundColor: '#f2f2f2', - }, - sheetItemText: { - fontSize: 16, - color: '#222', - }, }); export default App; diff --git a/android/app/src/main/java/com/culpeos/app/MainActivity.kt b/android/app/src/main/java/com/culpeos/app/MainActivity.kt index 16c43f2..50f70ba 100644 --- a/android/app/src/main/java/com/culpeos/app/MainActivity.kt +++ b/android/app/src/main/java/com/culpeos/app/MainActivity.kt @@ -1,5 +1,6 @@ package com.culpeos.app +import android.os.Bundle import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled @@ -13,6 +14,15 @@ class MainActivity : ReactActivity() { */ override fun getMainComponentName(): String = "Culpeos" + /** + * react-native-screens (used by the navigation stack) requires Android to NOT restore the + * fragment hierarchy from a saved instance state, or it crashes recreating native screens. + * Passing null here lets React Native rebuild the view tree itself. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(null) + } + /** * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] diff --git a/bun.lock b/bun.lock index 01aa667..399ea09 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "Culpeos", @@ -8,11 +7,14 @@ "@apollo/client": "^3.11.0", "@maplibre/maplibre-react-native": "^11.2.1", "@react-native/new-app-screen": "0.85.3", + "@react-navigation/native": "^7.2.5", + "@react-navigation/native-stack": "^7.16.0", "graphql": "^16.9.0", "react": "19.2.3", "react-native": "0.85.3", "react-native-encrypted-storage": "^4.0.3", "react-native-safe-area-context": "^5.5.2", + "react-native-screens": "^4.25.2", }, "devDependencies": { "@babel/core": "^7.25.2", @@ -514,6 +516,16 @@ "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.85.3", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "0.85.3" }, "optionalPeers": ["@types/react"] }, "sha512-dsCjI//OIPEUJMyNHp4l7zNLVjCx7bcaRUceOCkU+IB17hkbtbGWvi7HjGFSzy7FJGmS/MOlcfpb72xXiy1Oig=="], + "@react-navigation/core": ["@react-navigation/core@7.17.5", "", { "dependencies": { "@react-navigation/routers": "^7.5.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg=="], + + "@react-navigation/elements": ["@react-navigation/elements@2.9.19", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.5", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-gBUvCZuUkOGw1KpLQEZIkByUz8RYPwXeoA6mZFJy9K1mxd8GdqHDMFCIoB0lfPz9rgrHj99RvtdlGZ/ZzkZv2A=="], + + "@react-navigation/native": ["@react-navigation/native@7.2.5", "", { "dependencies": { "@react-navigation/core": "^7.17.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg=="], + + "@react-navigation/native-stack": ["@react-navigation/native-stack@7.16.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.19", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.2.5", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-wM21rHYR2XifjDnKLrr3HeHUeGsWQZJRwPqEzy1Vp/a9k3ieiwTGpmpDItD/jtERH9qkYESwDPO6oEtrVBEpQg=="], + + "@react-navigation/routers": ["@react-navigation/routers@7.5.5", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ=="], + "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], @@ -724,10 +736,14 @@ "collect-v8-coverage": ["collect-v8-coverage@1.0.3", "", {}, "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], @@ -776,6 +792,8 @@ "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -834,7 +852,7 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -852,6 +870,8 @@ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -872,6 +892,8 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], + "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -1208,6 +1230,8 @@ "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], @@ -1308,6 +1332,8 @@ "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], + "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1322,6 +1348,8 @@ "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], + "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], + "react-is": ["react-is@19.2.6", "", {}, "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw=="], "react-native": ["react-native@0.85.3", "", { "dependencies": { "@react-native/assets-registry": "0.85.3", "@react-native/codegen": "0.85.3", "@react-native/community-cli-plugin": "0.85.3", "@react-native/gradle-plugin": "0.85.3", "@react-native/js-polyfills": "0.85.3", "@react-native/normalize-colors": "0.85.3", "@react-native/virtualized-lists": "0.85.3", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.33.3", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "250829098.0.10", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.3", "metro-source-map": "^0.84.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@react-native/jest-preset": "0.85.3", "@types/react": "^19.1.1", "react": "^19.2.3" }, "optionalPeers": ["@react-native/jest-preset", "@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-HN/fGC+3nZVcDNcw7gfbM/DuqZAvI9Mz+/SxuhODaua4JY0BPzhfTzWXRyTR4mRgMHmShTPpH2PYMTxvZrsdZA=="], @@ -1330,6 +1358,8 @@ "react-native-safe-area-context": ["react-native-safe-area-context@5.8.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-t+ZsAVzY/wWzzx34vqGbo3/as9EEESJdbyZNL7Yg5EYX+toYMtMqFoDDCvqZUi35eeGVsXc6pAaEk4edMwbuCQ=="], + "react-native-screens": ["react-native-screens@4.25.2", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": ">=0.82.0" } }, "sha512-1Nj1fusFd+rIMKU/qC9yGKVG+3ofh11d3OdBQKL1iVvQfKvcB8vhvTGQf2TkfxW3bamxN+hCZIXmNuU0mRkyDg=="], + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "react-test-renderer": ["react-test-renderer@19.2.3", "", { "dependencies": { "react-is": "^19.2.3", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw=="], @@ -1402,6 +1432,8 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sf-symbols-typescript": ["sf-symbols-typescript@2.2.0", "", {}, "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -1418,6 +1450,8 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -1430,6 +1464,8 @@ "source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], + "sponge-case": ["sponge-case@1.0.1", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA=="], "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], @@ -1442,6 +1478,8 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + "string-env-interpolation": ["string-env-interpolation@1.0.1", "", {}, "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg=="], "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], @@ -1534,6 +1572,10 @@ "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], + "use-latest-callback": ["use-latest-callback@0.2.6", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], @@ -1546,6 +1588,8 @@ "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + "warn-once": ["warn-once@0.1.1", "", {}, "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -1696,12 +1740,8 @@ "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "chrome-launcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "chromium-edge-launcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -1712,6 +1752,8 @@ "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -1776,6 +1818,8 @@ "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "simple-swizzle/is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], diff --git a/jest.config.js b/jest.config.js index b342b7c..01ec325 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,9 @@ module.exports = { preset: '@react-native/jest-preset', setupFiles: ['/jest.setup.js'], + // @react-navigation and react-native-screens ship ESM that must be + // transpiled; widen the preset's default allowlist to include them. + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@react-navigation|react-native-screens)/)', + ], }; diff --git a/package.json b/package.json index 6447b2e..c510f66 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,14 @@ "@apollo/client": "^3.11.0", "@maplibre/maplibre-react-native": "^11.2.1", "@react-native/new-app-screen": "0.85.3", + "@react-navigation/native": "^7.2.5", + "@react-navigation/native-stack": "^7.16.0", "graphql": "^16.9.0", "react": "19.2.3", "react-native": "0.85.3", "react-native-encrypted-storage": "^4.0.3", - "react-native-safe-area-context": "^5.5.2" + "react-native-safe-area-context": "^5.5.2", + "react-native-screens": "^4.25.2" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/src/account/AccountMenu.tsx b/src/account/AccountMenu.tsx new file mode 100644 index 0000000..73d8a35 --- /dev/null +++ b/src/account/AccountMenu.tsx @@ -0,0 +1,100 @@ +import {useState} from 'react'; +import {Pressable, StyleSheet, Text} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import {logout} from '../auth/authClient'; +import {useAuth} from '../auth/tokenStore'; +import {Sheet} from '../ui/Sheet'; + +/** + * Account avatar + sheet, pinned top-right. Lives inside the map screen (rather + * than as a root-level sibling) so pushed detail screens cover it. + */ +export function AccountMenu() { + const safeAreaInsets = useSafeAreaInsets(); + const [open, setOpen] = useState(false); + const auth = useAuth(); + + if (!auth) { + return null; + } + const {user} = auth; + const initial = user.email.trim().charAt(0).toUpperCase() || '?'; + + return ( + <> + setOpen(true)} + style={({pressed}) => [ + styles.avatarButton, + {top: safeAreaInsets.top + 12}, + pressed && styles.avatarPressed, + ]}> + {initial} + + setOpen(false)} + scrimAccessibilityLabel="Close account menu"> + + {user.email} + + { + setOpen(false); + logout(); + }} + style={({pressed}) => [ + styles.sheetItem, + pressed && styles.sheetItemPressed, + ]}> + Log out + + + + ); +} + +const styles = StyleSheet.create({ + avatarButton: { + position: 'absolute', + right: 16, + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#1d6fe0', + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOpacity: 0.2, + shadowRadius: 4, + shadowOffset: {width: 0, height: 2}, + elevation: 4, + }, + avatarPressed: {opacity: 0.8}, + avatarText: { + color: '#ffffff', + fontSize: 16, + fontWeight: '600', + }, + sheetEmail: { + fontSize: 12, + color: '#666', + paddingHorizontal: 12, + paddingBottom: 8, + }, + sheetItem: { + paddingHorizontal: 12, + paddingVertical: 14, + borderRadius: 8, + }, + sheetItemPressed: { + backgroundColor: '#f2f2f2', + }, + sheetItemText: { + fontSize: 16, + color: '#222', + }, +}); diff --git a/src/map/ElementDetailModal.tsx b/src/map/ElementDetailScreen.tsx similarity index 92% rename from src/map/ElementDetailModal.tsx rename to src/map/ElementDetailScreen.tsx index b393d82..8461cc8 100644 --- a/src/map/ElementDetailModal.tsx +++ b/src/map/ElementDetailScreen.tsx @@ -1,3 +1,4 @@ +import type {NativeStackScreenProps} from '@react-navigation/native-stack'; import { ActivityIndicator, Image, @@ -12,29 +13,24 @@ import { type ElementDetailQuery, useElementDetailQuery, } from '../graphql/__generated__/types'; -import {Sheet} from '../ui/Sheet'; +import type {RootStackParamList} from '../navigation/types'; -type Props = { - elementId: string | null; - onClose: () => void; -}; +type Props = NativeStackScreenProps; type ElementDetail = ElementDetailQuery['element']; -export function ElementDetailModal({elementId, onClose}: Props) { - const {data, loading} = useElementDetailQuery({ - variables: {id: elementId ?? ''}, - skip: !elementId, - }); +export function ElementDetailScreen({route, navigation}: Props) { + const {elementId} = route.params; + const {data, loading} = useElementDetailQuery({variables: {id: elementId}}); return ( - + navigation.goBack()} /> - + ); } @@ -167,6 +163,10 @@ function formatSchedule(schedule: NonNullable) { } const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: '#ffffff', + }, container: { flex: 1, backgroundColor: '#ffffff', diff --git a/src/map/MapScreen.tsx b/src/map/MapScreen.tsx index 8bc99c9..1cc0e27 100644 --- a/src/map/MapScreen.tsx +++ b/src/map/MapScreen.tsx @@ -8,16 +8,18 @@ import { useCurrentPosition, type ViewStateChangeEvent, } from '@maplibre/maplibre-react-native'; +import {useNavigation} from '@react-navigation/native'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent} from 'react-native'; import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import {AccountMenu} from '../account/AccountMenu'; import { type ElementsQuery, type ElementsQueryVariables, useElementsQuery, } from '../graphql/__generated__/types'; -import {ElementDetailModal} from './ElementDetailModal'; +import type {RootStackNavigation} from '../navigation/types'; import {ElementPreviewCard} from './ElementPreviewCard'; import {zoomForPlaceTypes} from './placeZoom'; import { @@ -49,6 +51,7 @@ const OFF_CENTER_THRESHOLD = 0.2; const BOTTOM_MARGIN = 16; export function MapScreen() { + const navigation = useNavigation(); const cameraRef = useRef(null); // Gate the bounds fetch + viewport-save until we know the map is sitting on // a real location — either a restored viewport or a fly-to-user. Prevents @@ -58,14 +61,14 @@ export function MapScreen() { const [savedViewport, setSavedViewport] = useState< Viewport | null | undefined >(undefined); - // The element selected on the map (shows the bottom preview card). Separate - // from the element whose full-detail modal is open, so a located element can - // fall back to its preview card after the modal closes while a location-less - // one (which has no map presence) returns straight to the plain map. + // The element selected on the map (shows the bottom preview card). Kept in + // map state while the full-detail screen is pushed on top, so a located + // element falls back to its preview card when the detail screen pops; a + // location-less one (no map presence) is never selected, so popping returns + // straight to the plain map. const [selectedElementId, setSelectedElementId] = useState( null, ); - const [detailElementId, setDetailElementId] = useState(null); // When set, the map is filtered to a single trip: only that trip's elements // are shown and the bounds-based query is paused. const [tripFilter, setTripFilter] = useState<{ @@ -265,30 +268,33 @@ export function MapScreen() { } }, [tripFilter, tripElements, elementsLoading]); - const handleSelectElement = useCallback((element: SearchElement) => { - setTripFilter(null); - if (element.location) { - const {longitude, latitude} = element.location; - // Seed the marker set so the pin is present immediately, before the - // bounds query around the new center returns. - setElementsById(prev => { - const next = new Map(prev); - next.set(element.id, element); - return next; - }); - setSelectedElementId(element.id); - cameraRef.current?.flyTo({ - center: [longitude, latitude], - zoom: ELEMENT_ZOOM, - duration: 1200, - }); - } else { - // No coordinates to fly to — a preview card pinned over an unrelated map - // view would be misleading, so go straight to the full details. - setSelectedElementId(null); - setDetailElementId(element.id); - } - }, []); + const handleSelectElement = useCallback( + (element: SearchElement) => { + setTripFilter(null); + if (element.location) { + const {longitude, latitude} = element.location; + // Seed the marker set so the pin is present immediately, before the + // bounds query around the new center returns. + setElementsById(prev => { + const next = new Map(prev); + next.set(element.id, element); + return next; + }); + setSelectedElementId(element.id); + cameraRef.current?.flyTo({ + center: [longitude, latitude], + zoom: ELEMENT_ZOOM, + duration: 1200, + }); + } else { + // No coordinates to fly to — a preview card pinned over an unrelated + // map view would be misleading, so go straight to the full details. + setSelectedElementId(null); + navigation.navigate('ElementDetail', {elementId: element.id}); + } + }, + [navigation], + ); const handleSelectTrip = useCallback((trip: SearchTrip) => { setSelectedElementId(null); @@ -365,7 +371,9 @@ export function MapScreen() { elementId={selectedElementId} bottomOffset={safeAreaInsets.bottom + BOTTOM_MARGIN} onClose={() => setSelectedElementId(null)} - onExpand={() => setDetailElementId(selectedElementId)} + onExpand={() => + navigation.navigate('ElementDetail', {elementId: selectedElementId}) + } /> ) : null} {tripFilter ? ( @@ -382,10 +390,7 @@ export function MapScreen() { onSelectTrip={handleSelectTrip} onSelectPlace={handleSelectPlace} /> - setDetailElementId(null)} - /> + ); } diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx new file mode 100644 index 0000000..16277b3 --- /dev/null +++ b/src/navigation/RootNavigator.tsx @@ -0,0 +1,21 @@ +import {createNativeStackNavigator} from '@react-navigation/native-stack'; +import {ElementDetailScreen} from '../map/ElementDetailScreen'; +import {MapScreen} from '../map/MapScreen'; +import type {RootStackParamList} from './types'; + +const Stack = createNativeStackNavigator(); + +/** + * The authenticated app's screen stack. Headers are hidden — each screen draws + * its own chrome edge-to-edge and applies its own safe-area padding. The detail + * screen slides in from the right as a standard forward push. + */ +export function RootNavigator() { + return ( + + + + + ); +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts new file mode 100644 index 0000000..9cd71db --- /dev/null +++ b/src/navigation/types.ts @@ -0,0 +1,13 @@ +import type {NativeStackNavigationProp} from '@react-navigation/native-stack'; + +/** + * Routes for the authenticated app. The map is the root; detail surfaces are + * pushed on top so they cover the map chrome (account menu, search) instead of + * fighting it for z-order as the old in-tree modal did. + */ +export type RootStackParamList = { + Map: undefined; + ElementDetail: {elementId: string}; +}; + +export type RootStackNavigation = NativeStackNavigationProp; diff --git a/src/ui/Sheet.tsx b/src/ui/Sheet.tsx index 5b2cdfe..aa4891d 100644 --- a/src/ui/Sheet.tsx +++ b/src/ui/Sheet.tsx @@ -1,61 +1,48 @@ import {type ReactNode, useEffect, useRef, useState} from 'react'; -import { - Animated, - BackHandler, - Pressable, - StyleSheet, - useWindowDimensions, - View, -} from 'react-native'; +import {Animated, BackHandler, Pressable, StyleSheet, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; const AnimatedPressable = Animated.createAnimatedComponent(Pressable); -type SheetVariant = 'bottom' | 'fullscreen'; - type Props = { visible: boolean; onClose: () => void; - /** `bottom` (default): dim scrim + bottom-anchored card. `fullscreen`: opaque full-screen. */ - variant?: SheetVariant; - /** Tap the scrim to close (bottom variant only). Defaults to true. */ + /** Tap the scrim to close. Defaults to true. */ dismissOnScrimPress?: boolean; - /** Accessibility label for the scrim's close affordance (bottom variant). */ + /** Accessibility label for the scrim's close affordance. */ scrimAccessibilityLabel?: string; children: ReactNode; }; /** - * In-tree modal sheet. Rendered as an absolute overlay (not a React Native - * `Modal`) so it inherits the app's edge-to-edge window and draws behind the - * status and navigation bars. RN's `Modal` renders in a separate window whose - * `statusBarTranslucent`/`navigationBarTranslucent` props don't reliably do - * this on RN 0.85 + Android 15, which left scrims and sheets stopping at the - * system bars. + * In-tree bottom sheet: a dim scrim plus a bottom-anchored card. Rendered as an + * absolute overlay (not a React Native `Modal`) so it inherits the app's + * edge-to-edge window and draws behind the status and navigation bars. RN's + * `Modal` renders in a separate window whose `statusBarTranslucent`/ + * `navigationBarTranslucent` props don't reliably do this on RN 0.85 + + * Android 15, which left scrims and sheets stopping at the system bars. * * Owns the slide animation, hardware-back handling, and pointer-event gating; - * callers supply the content (and their own safe-area padding for fullscreen). + * callers supply the content. */ export function Sheet({ visible, onClose, - variant = 'bottom', dismissOnScrimPress = true, scrimAccessibilityLabel = 'Close', children, }: Props) { const safeAreaInsets = useSafeAreaInsets(); - const {height} = useWindowDimensions(); const anim = useRef(new Animated.Value(0)).current; const [sheetHeight, setSheetHeight] = useState(0); useEffect(() => { Animated.timing(anim, { toValue: visible ? 1 : 0, - duration: variant === 'fullscreen' ? 250 : 220, + duration: 220, useNativeDriver: true, }).start(); - }, [visible, variant, anim]); + }, [visible, anim]); // The hardware back button closes the sheet (RN Modal used to handle this). useEffect(() => { @@ -69,24 +56,6 @@ export function Sheet({ return () => sub.remove(); }, [visible, onClose]); - if (variant === 'fullscreen') { - const translateY = anim.interpolate({ - inputRange: [0, 1], - outputRange: [height, 0], - }); - return ( - - {children} - - ); - } - const translateY = anim.interpolate({ inputRange: [0, 1], outputRange: [sheetHeight || 320, 0], @@ -115,9 +84,6 @@ export function Sheet({ } const styles = StyleSheet.create({ - fullscreen: { - backgroundColor: '#ffffff', - }, scrim: { position: 'absolute', top: 0,