1- import { createBottomTabNavigator } from "@react-navigation/bottom-tabs" ;
2- import { NavigationContainer } from "@react-navigation/native" ;
31import Constants from "expo-constants" ;
42import * as Device from "expo-device" ;
5- import { GlassView } from "expo-glass-effect" ;
63import * as SplashScreen from "expo-splash-screen" ;
74import { StatusBar } from "expo-status-bar" ;
85import { useEffect , useMemo , useState } from "react" ;
9- import { Platform , StyleSheet , Text , View } from "react-native" ;
6+ import { Platform , Pressable , StyleSheet , Text , View } from "react-native" ;
107import { SafeAreaProvider , SafeAreaView } from "react-native-safe-area-context" ;
8+ import { Tabs , type TabSelectedEvent } from "react-native-screens" ;
119import { Exceptionless } from "@exceptionless/react-native" ;
1210
1311import { callbackLog , getLogEntries , subscribeToLogs } from "./logging" ;
1412import ErrorsScreen from "./screens/ErrorsScreen" ;
1513import EventsScreen from "./screens/EventsScreen" ;
1614import LogsScreen from "./screens/LogsScreen" ;
1715
18- type TabParamList = {
19- Errors : undefined ;
20- Events : undefined ;
21- Logs : undefined ;
22- } ;
16+ type TabKey = "Errors" | "Events" | "Logs" ;
2317
24- const Tab = createBottomTabNavigator < TabParamList > ( ) ;
18+ const appTabs : Array < { icon : string ; key : TabKey ; title : string } > = [
19+ { icon : "exclamationmark.triangle" , key : "Errors" , title : "Errors" } ,
20+ { icon : "tray.and.arrow.up" , key : "Events" , title : "Events" } ,
21+ { icon : "list.bullet.rectangle" , key : "Logs" , title : "Logs" }
22+ ] ;
2523
2624const serverUrl = getServerUrl ( ) ;
2725
28- function TabIcon ( { label, focused } : { label : string ; focused : boolean } ) {
29- return < Text style = { [ styles . tabIcon , focused && styles . tabIconFocused ] } > { label } </ Text > ;
30- }
31-
3226/**
3327 * Resolves the dev server URL based on the current platform.
3428 * - Web and iOS Simulator: localhost reaches the host Mac.
@@ -87,6 +81,67 @@ function TopDiagnostics() {
8781 ) ;
8882}
8983
84+ function NativeTabs ( ) {
85+ const [ selectedTab , setSelectedTab ] = useState < TabKey > ( "Errors" ) ;
86+ const [ baseProvenance , setBaseProvenance ] = useState ( 0 ) ;
87+ const navStateRequest = useMemo ( ( ) => ( { baseProvenance, selectedScreenKey : selectedTab } ) , [ baseProvenance , selectedTab ] ) ;
88+
89+ const handleTabSelected = ( event : { nativeEvent : TabSelectedEvent } ) => {
90+ setSelectedTab ( event . nativeEvent . selectedScreenKey as TabKey ) ;
91+ setBaseProvenance ( event . nativeEvent . provenance ) ;
92+ } ;
93+
94+ if ( Platform . OS === "web" ) {
95+ const ActiveScreen = selectedTab === "Errors" ? ErrorsScreen : selectedTab === "Events" ? EventsScreen : LogsScreen ;
96+
97+ return (
98+ < View style = { styles . webTabs } >
99+ < View style = { styles . webTabContent } >
100+ < ActiveScreen />
101+ </ View >
102+ < View style = { styles . webTabBar } >
103+ { appTabs . map ( ( tab ) => (
104+ < Pressable
105+ key = { tab . key }
106+ accessibilityRole = "tab"
107+ accessibilityState = { { selected : selectedTab === tab . key } }
108+ onPress = { ( ) => setSelectedTab ( tab . key ) }
109+ style = { styles . webTabButton }
110+ >
111+ < Text style = { [ styles . webTabLabel , selectedTab === tab . key && styles . webTabLabelActive ] } > { tab . title } </ Text >
112+ </ Pressable >
113+ ) ) }
114+ </ View >
115+ </ View >
116+ ) ;
117+ }
118+
119+ return (
120+ < Tabs . Host
121+ colorScheme = "light"
122+ ios = { {
123+ tabBarControllerMode : "tabBar" ,
124+ tabBarMinimizeBehavior : "never" ,
125+ tabBarTintColor : "#0f172a"
126+ } }
127+ nativeContainerStyle = { styles . nativeTabsContainer }
128+ navStateRequest = { navStateRequest }
129+ onTabSelected = { handleTabSelected }
130+ rejectStaleNavStateUpdates
131+ >
132+ { appTabs . map ( ( tab ) => {
133+ const Screen = tab . key === "Errors" ? ErrorsScreen : tab . key === "Events" ? EventsScreen : LogsScreen ;
134+
135+ return (
136+ < Tabs . Screen ios = { { icon : { name : tab . icon , type : "sfSymbol" } } } key = { tab . key } screenKey = { tab . key } style = { styles . nativeTabScreen } title = { tab . title } >
137+ < Screen />
138+ </ Tabs . Screen >
139+ ) ;
140+ } ) }
141+ </ Tabs . Host >
142+ ) ;
143+ }
144+
90145export default function App ( ) {
91146 useEffect ( ( ) => {
92147 void Exceptionless . startup ( ( config ) => {
@@ -101,47 +156,11 @@ export default function App() {
101156
102157 return (
103158 < SafeAreaProvider >
104- < NavigationContainer >
105- < View style = { styles . appShell } >
106- < TopDiagnostics />
107- < Tab . Navigator
108- screenOptions = { {
109- headerShown : false ,
110- tabBarActiveTintColor : "#0f172a" ,
111- tabBarInactiveTintColor : "#64748b" ,
112- tabBarLabelStyle : styles . tabLabel ,
113- tabBarStyle : styles . tabBar ,
114- tabBarBackground : ( ) => < GlassView glassEffectStyle = "regular" isInteractive style = { StyleSheet . absoluteFill } tintColor = "rgba(255,255,255,0.58)" />
115- } }
116- >
117- < Tab . Screen
118- name = "Errors"
119- component = { ErrorsScreen }
120- options = { {
121- title : "Errors" ,
122- tabBarIcon : ( { focused } ) => < TabIcon label = "!" focused = { focused } />
123- } }
124- />
125- < Tab . Screen
126- name = "Events"
127- component = { EventsScreen }
128- options = { {
129- title : "Events" ,
130- tabBarIcon : ( { focused } ) => < TabIcon label = "|" focused = { focused } />
131- } }
132- />
133- < Tab . Screen
134- name = "Logs"
135- component = { LogsScreen }
136- options = { {
137- title : "Logs" ,
138- tabBarIcon : ( { focused } ) => < TabIcon label = "#" focused = { focused } />
139- } }
140- />
141- </ Tab . Navigator >
142- </ View >
143- < StatusBar style = "auto" />
144- </ NavigationContainer >
159+ < View style = { styles . appShell } >
160+ < TopDiagnostics />
161+ < NativeTabs />
162+ </ View >
163+ < StatusBar style = "auto" />
145164 </ SafeAreaProvider >
146165 ) ;
147166}
@@ -210,31 +229,40 @@ const styles = StyleSheet.create({
210229 lineHeight : 15 ,
211230 marginTop : 8
212231 } ,
213- tabBar : {
214- backgroundColor : "rgba(255,255,255,0.5)" ,
215- borderTopColor : "rgba(148,163,184,0.22)" ,
232+ nativeTabsContainer : {
233+ backgroundColor : "#fff"
234+ } ,
235+ nativeTabScreen : {
236+ backgroundColor : "#fff"
237+ } ,
238+ webTabs : {
239+ flex : 1
240+ } ,
241+ webTabBar : {
242+ alignItems : "center" ,
243+ backgroundColor : "rgba(255,255,255,0.92)" ,
244+ borderTopColor : "#e5e7eb" ,
216245 borderTopWidth : StyleSheet . hairlineWidth ,
217- elevation : 0 ,
218- height : 78 ,
219- paddingBottom : 14 ,
220- paddingTop : 8 ,
221- position : "absolute" ,
222- shadowColor : "#0f172a" ,
223- shadowOffset : { height : - 4 , width : 0 } ,
224- shadowOpacity : 0.08 ,
225- shadowRadius : 18
226- } ,
227- tabIcon : {
228- color : "#64748b" ,
229- fontSize : 18 ,
230- fontWeight : "800" ,
231- lineHeight : 20
246+ flexDirection : "row" ,
247+ minHeight : 72 ,
248+ paddingBottom : 12 ,
249+ paddingTop : 8
232250 } ,
233- tabIconFocused : {
234- color : "#0f172a"
251+ webTabButton : {
252+ alignItems : "center" ,
253+ flex : 1 ,
254+ justifyContent : "center" ,
255+ minHeight : 44
235256 } ,
236- tabLabel : {
257+ webTabContent : {
258+ flex : 1
259+ } ,
260+ webTabLabel : {
261+ color : "#64748b" ,
237262 fontSize : 12 ,
238263 fontWeight : "700"
264+ } ,
265+ webTabLabelActive : {
266+ color : "#0f172a"
239267 }
240268} ) ;
0 commit comments