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,