Skip to content

Commit 3338cd7

Browse files
dfallingclaude
andauthored
Make element details a navigation screen instead of an in-tree modal (#17)
The account menu (a root-level sibling rendered after MapScreen) always painted over the hand-rolled fullscreen detail Sheet, since RN paint order follows tree order and there is no cross-tree z-index. Promoting element details to a real native-stack screen lets it cover the whole map screen, account menu included, and sets up Trip/Place detail to follow the same path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a7c77f6 commit 3338cd7

11 files changed

Lines changed: 277 additions & 203 deletions

File tree

App.tsx

Lines changed: 9 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,21 @@
55
*/
66

77
import {ApolloProvider} from '@apollo/client';
8-
import {useEffect, useState} from 'react';
8+
import {NavigationContainer} from '@react-navigation/native';
9+
import {useEffect} from 'react';
910
import {
1011
ActivityIndicator,
11-
Pressable,
1212
StatusBar,
1313
StyleSheet,
14-
Text,
1514
useColorScheme,
1615
View,
1716
} from 'react-native';
18-
import {
19-
SafeAreaProvider,
20-
useSafeAreaInsets,
21-
} from 'react-native-safe-area-context';
22-
import {apolloClient, logout} from './src/auth/authClient';
17+
import {SafeAreaProvider} from 'react-native-safe-area-context';
18+
import {apolloClient} from './src/auth/authClient';
2319
import {useDeepLinkListener} from './src/auth/deepLinks';
2420
import {LoginScreen} from './src/auth/LoginScreen';
25-
import {
26-
type AuthUser,
27-
tokenStore,
28-
useAuth,
29-
useAuthHydrated,
30-
} from './src/auth/tokenStore';
31-
import {MapScreen} from './src/map/MapScreen';
32-
import {Sheet} from './src/ui/Sheet';
21+
import {tokenStore, useAuth, useAuthHydrated} from './src/auth/tokenStore';
22+
import {RootNavigator} from './src/navigation/RootNavigator';
3323

3424
function App() {
3525
const isDarkMode = useColorScheme() === 'dark';
@@ -67,53 +57,9 @@ function AppContent() {
6757
}
6858

6959
return (
70-
<View style={styles.container}>
71-
<MapScreen />
72-
<AccountMenu user={auth.user} />
73-
</View>
74-
);
75-
}
76-
77-
function AccountMenu({user}: {user: AuthUser}) {
78-
const safeAreaInsets = useSafeAreaInsets();
79-
const [open, setOpen] = useState(false);
80-
81-
const initial = user.email.trim().charAt(0).toUpperCase() || '?';
82-
83-
return (
84-
<>
85-
<Pressable
86-
accessibilityLabel="Account menu"
87-
accessibilityRole="button"
88-
onPress={() => setOpen(true)}
89-
style={({pressed}) => [
90-
styles.avatarButton,
91-
{top: safeAreaInsets.top + 12},
92-
pressed && styles.avatarPressed,
93-
]}>
94-
<Text style={styles.avatarText}>{initial}</Text>
95-
</Pressable>
96-
<Sheet
97-
visible={open}
98-
onClose={() => setOpen(false)}
99-
scrimAccessibilityLabel="Close account menu">
100-
<Text style={styles.sheetEmail} numberOfLines={1}>
101-
{user.email}
102-
</Text>
103-
<Pressable
104-
accessibilityRole="button"
105-
onPress={() => {
106-
setOpen(false);
107-
logout();
108-
}}
109-
style={({pressed}) => [
110-
styles.sheetItem,
111-
pressed && styles.sheetItemPressed,
112-
]}>
113-
<Text style={styles.sheetItemText}>Log out</Text>
114-
</Pressable>
115-
</Sheet>
116-
</>
60+
<NavigationContainer>
61+
<RootNavigator />
62+
</NavigationContainer>
11763
);
11864
}
11965

@@ -125,45 +71,6 @@ const styles = StyleSheet.create({
12571
alignItems: 'center',
12672
justifyContent: 'center',
12773
},
128-
avatarButton: {
129-
position: 'absolute',
130-
right: 16,
131-
width: 40,
132-
height: 40,
133-
borderRadius: 20,
134-
backgroundColor: '#1d6fe0',
135-
alignItems: 'center',
136-
justifyContent: 'center',
137-
shadowColor: '#000',
138-
shadowOpacity: 0.2,
139-
shadowRadius: 4,
140-
shadowOffset: {width: 0, height: 2},
141-
elevation: 4,
142-
},
143-
avatarPressed: {opacity: 0.8},
144-
avatarText: {
145-
color: '#ffffff',
146-
fontSize: 16,
147-
fontWeight: '600',
148-
},
149-
sheetEmail: {
150-
fontSize: 12,
151-
color: '#666',
152-
paddingHorizontal: 12,
153-
paddingBottom: 8,
154-
},
155-
sheetItem: {
156-
paddingHorizontal: 12,
157-
paddingVertical: 14,
158-
borderRadius: 8,
159-
},
160-
sheetItemPressed: {
161-
backgroundColor: '#f2f2f2',
162-
},
163-
sheetItemText: {
164-
fontSize: 16,
165-
color: '#222',
166-
},
16774
});
16875

16976
export default App;

android/app/src/main/java/com/culpeos/app/MainActivity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.culpeos.app
22

3+
import android.os.Bundle
34
import com.facebook.react.ReactActivity
45
import com.facebook.react.ReactActivityDelegate
56
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
@@ -13,6 +14,15 @@ class MainActivity : ReactActivity() {
1314
*/
1415
override fun getMainComponentName(): String = "Culpeos"
1516

17+
/**
18+
* react-native-screens (used by the navigation stack) requires Android to NOT restore the
19+
* fragment hierarchy from a saved instance state, or it crashes recreating native screens.
20+
* Passing null here lets React Native rebuild the view tree itself.
21+
*/
22+
override fun onCreate(savedInstanceState: Bundle?) {
23+
super.onCreate(null)
24+
}
25+
1626
/**
1727
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
1828
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]

bun.lock

Lines changed: 50 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
module.exports = {
22
preset: '@react-native/jest-preset',
33
setupFiles: ['<rootDir>/jest.setup.js'],
4+
// @react-navigation and react-native-screens ship ESM that must be
5+
// transpiled; widen the preset's default allowlist to include them.
6+
transformIgnorePatterns: [
7+
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@react-navigation|react-native-screens)/)',
8+
],
49
};

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
"@apollo/client": "^3.11.0",
1717
"@maplibre/maplibre-react-native": "^11.2.1",
1818
"@react-native/new-app-screen": "0.85.3",
19+
"@react-navigation/native": "^7.2.5",
20+
"@react-navigation/native-stack": "^7.16.0",
1921
"graphql": "^16.9.0",
2022
"react": "19.2.3",
2123
"react-native": "0.85.3",
2224
"react-native-encrypted-storage": "^4.0.3",
23-
"react-native-safe-area-context": "^5.5.2"
25+
"react-native-safe-area-context": "^5.5.2",
26+
"react-native-screens": "^4.25.2"
2427
},
2528
"devDependencies": {
2629
"@babel/core": "^7.25.2",

src/account/AccountMenu.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {useState} from 'react';
2+
import {Pressable, StyleSheet, Text} from 'react-native';
3+
import {useSafeAreaInsets} from 'react-native-safe-area-context';
4+
import {logout} from '../auth/authClient';
5+
import {useAuth} from '../auth/tokenStore';
6+
import {Sheet} from '../ui/Sheet';
7+
8+
/**
9+
* Account avatar + sheet, pinned top-right. Lives inside the map screen (rather
10+
* than as a root-level sibling) so pushed detail screens cover it.
11+
*/
12+
export function AccountMenu() {
13+
const safeAreaInsets = useSafeAreaInsets();
14+
const [open, setOpen] = useState(false);
15+
const auth = useAuth();
16+
17+
if (!auth) {
18+
return null;
19+
}
20+
const {user} = auth;
21+
const initial = user.email.trim().charAt(0).toUpperCase() || '?';
22+
23+
return (
24+
<>
25+
<Pressable
26+
accessibilityLabel="Account menu"
27+
accessibilityRole="button"
28+
onPress={() => setOpen(true)}
29+
style={({pressed}) => [
30+
styles.avatarButton,
31+
{top: safeAreaInsets.top + 12},
32+
pressed && styles.avatarPressed,
33+
]}>
34+
<Text style={styles.avatarText}>{initial}</Text>
35+
</Pressable>
36+
<Sheet
37+
visible={open}
38+
onClose={() => setOpen(false)}
39+
scrimAccessibilityLabel="Close account menu">
40+
<Text style={styles.sheetEmail} numberOfLines={1}>
41+
{user.email}
42+
</Text>
43+
<Pressable
44+
accessibilityRole="button"
45+
onPress={() => {
46+
setOpen(false);
47+
logout();
48+
}}
49+
style={({pressed}) => [
50+
styles.sheetItem,
51+
pressed && styles.sheetItemPressed,
52+
]}>
53+
<Text style={styles.sheetItemText}>Log out</Text>
54+
</Pressable>
55+
</Sheet>
56+
</>
57+
);
58+
}
59+
60+
const styles = StyleSheet.create({
61+
avatarButton: {
62+
position: 'absolute',
63+
right: 16,
64+
width: 40,
65+
height: 40,
66+
borderRadius: 20,
67+
backgroundColor: '#1d6fe0',
68+
alignItems: 'center',
69+
justifyContent: 'center',
70+
shadowColor: '#000',
71+
shadowOpacity: 0.2,
72+
shadowRadius: 4,
73+
shadowOffset: {width: 0, height: 2},
74+
elevation: 4,
75+
},
76+
avatarPressed: {opacity: 0.8},
77+
avatarText: {
78+
color: '#ffffff',
79+
fontSize: 16,
80+
fontWeight: '600',
81+
},
82+
sheetEmail: {
83+
fontSize: 12,
84+
color: '#666',
85+
paddingHorizontal: 12,
86+
paddingBottom: 8,
87+
},
88+
sheetItem: {
89+
paddingHorizontal: 12,
90+
paddingVertical: 14,
91+
borderRadius: 8,
92+
},
93+
sheetItemPressed: {
94+
backgroundColor: '#f2f2f2',
95+
},
96+
sheetItemText: {
97+
fontSize: 16,
98+
color: '#222',
99+
},
100+
});
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {NativeStackScreenProps} from '@react-navigation/native-stack';
12
import {
23
ActivityIndicator,
34
Image,
@@ -12,29 +13,24 @@ import {
1213
type ElementDetailQuery,
1314
useElementDetailQuery,
1415
} from '../graphql/__generated__/types';
15-
import {Sheet} from '../ui/Sheet';
16+
import type {RootStackParamList} from '../navigation/types';
1617

17-
type Props = {
18-
elementId: string | null;
19-
onClose: () => void;
20-
};
18+
type Props = NativeStackScreenProps<RootStackParamList, 'ElementDetail'>;
2119

2220
type ElementDetail = ElementDetailQuery['element'];
2321

24-
export function ElementDetailModal({elementId, onClose}: Props) {
25-
const {data, loading} = useElementDetailQuery({
26-
variables: {id: elementId ?? ''},
27-
skip: !elementId,
28-
});
22+
export function ElementDetailScreen({route, navigation}: Props) {
23+
const {elementId} = route.params;
24+
const {data, loading} = useElementDetailQuery({variables: {id: elementId}});
2925

3026
return (
31-
<Sheet visible={elementId !== null} onClose={onClose} variant="fullscreen">
27+
<View style={styles.screen}>
3228
<ModalContents
3329
element={data?.element ?? null}
3430
loading={loading}
35-
onClose={onClose}
31+
onClose={() => navigation.goBack()}
3632
/>
37-
</Sheet>
33+
</View>
3834
);
3935
}
4036

@@ -167,6 +163,10 @@ function formatSchedule(schedule: NonNullable<ElementDetail['schedule']>) {
167163
}
168164

169165
const styles = StyleSheet.create({
166+
screen: {
167+
flex: 1,
168+
backgroundColor: '#ffffff',
169+
},
170170
container: {
171171
flex: 1,
172172
backgroundColor: '#ffffff',

0 commit comments

Comments
 (0)