diff --git a/docs/expo-router.md b/docs/expo-router.md index af5ce6792..14344a3f1 100644 --- a/docs/expo-router.md +++ b/docs/expo-router.md @@ -96,6 +96,18 @@ Keep workspace identity and retention outside native-stack `getId` and Navigation `getId`, and `getId` has broken Android native-stack/Fabric by reordering an already-mounted workspace screen. +Use `ThemedStack` from `packages/app/src/navigation/themed-stack.tsx` for every +Expo Router stack. React Navigation otherwise paints each native stack screen +with its light default background. A screen-level wrapper can hide that surface +while settled, but Android may expose it for one frame when navigation crosses +from a nested stack to its parent stack. This is especially visible when an +app-wide route such as `/new` opens from a dark workspace. + +Do not read the active theme with `useUnistyles()` in a layout to build +`screenOptions`. `ThemedStack` keeps that third-party prop theme-reactive through +a small `withUnistyles` boundary without subscribing the route tree itself to +every Unistyles runtime update. + ## Regression Shape Pure helper tests are useful but not enough. The failure mode here is native diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index e397affd5..db6db10e5 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -52,6 +52,7 @@ import { type StartupBlocker, } from "@/navigation/host-runtime-bootstrap"; import { registerWorkspaceRouteNavigationRef } from "@/navigation/workspace-route-navigation"; +import { ThemedStack } from "@/navigation/themed-stack"; import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; import { listenToDesktopEvent } from "@/desktop/electron/events"; import { updateDesktopWindowControls } from "@/desktop/electron/window"; @@ -784,21 +785,15 @@ function FaviconStatusSync() { return null; } +const ROOT_STACK_SCREEN_OPTIONS = { + headerShown: false, + animation: "none" as const, +}; + function RootStack() { const storeReady = useStoreReady(); - const { theme } = useUnistyles(); - const stackScreenOptions = useMemo( - () => ({ - headerShown: false, - animation: "none" as const, - contentStyle: { - backgroundColor: theme.colors.surface0, - }, - }), - [theme.colors.surface0], - ); return ( - + @@ -815,7 +810,7 @@ function RootStack() { - + ); } diff --git a/packages/app/src/app/h/[serverId]/_layout.tsx b/packages/app/src/app/h/[serverId]/_layout.tsx index 70f7fcf55..4cd3428e0 100644 --- a/packages/app/src/app/h/[serverId]/_layout.tsx +++ b/packages/app/src/app/h/[serverId]/_layout.tsx @@ -2,6 +2,7 @@ import { Redirect, Stack, useLocalSearchParams } from "expo-router"; import { useHostRuntimeBootstrapState } from "@/app/_layout"; import { HostRouteProvider } from "@/navigation/host-route-context"; import { resolveStartupRoute } from "@/navigation/host-runtime-bootstrap"; +import { ThemedStack } from "@/navigation/themed-stack"; import { useHostRegistryStatus, useHosts } from "@/runtime/host-runtime"; const HOST_STACK_SCREEN_OPTIONS = { @@ -33,14 +34,14 @@ function KnownHostRoute() { } const stack = ( - + - + ); if (!routeServerId) { diff --git a/packages/app/src/navigation/themed-stack.tsx b/packages/app/src/navigation/themed-stack.tsx new file mode 100644 index 000000000..cf83f701c --- /dev/null +++ b/packages/app/src/navigation/themed-stack.tsx @@ -0,0 +1,26 @@ +import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"; +import { Stack } from "expo-router"; +import { type ReactNode, useMemo } from "react"; +import { withUnistyles } from "react-native-unistyles"; + +interface ThemedStackBaseProps { + backgroundColor: string; + children?: ReactNode; + screenOptions?: NativeStackNavigationOptions; +} + +function ThemedStackBase({ backgroundColor, children, screenOptions }: ThemedStackBaseProps) { + const themedScreenOptions = useMemo( + () => ({ + ...screenOptions, + contentStyle: [{ backgroundColor }, screenOptions?.contentStyle], + }), + [backgroundColor, screenOptions], + ); + + return {children}; +} + +export const ThemedStack = withUnistyles(ThemedStackBase, (theme) => ({ + backgroundColor: theme.colors.surface0, +}));