fix(app): prevent white flashes between workspace routes

Nested native stacks fell back to the navigation library's light background during cross-stack swaps. Share the active app surface color across every stack without subscribing route layouts to all theme runtime changes.
This commit is contained in:
Mohamed Boudra
2026-07-12 00:27:40 +02:00
parent d28e174b38
commit 92d7066601
4 changed files with 49 additions and 15 deletions

View File

@@ -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

View File

@@ -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 (
<Stack screenOptions={stackScreenOptions}>
<ThemedStack screenOptions={ROOT_STACK_SCREEN_OPTIONS}>
<Stack.Screen name="index" />
<Stack.Protected guard={storeReady}>
<Stack.Screen name="welcome" />
@@ -815,7 +810,7 @@ function RootStack() {
<Stack.Screen name="h/[serverId]" />
<Stack.Screen name="settings/hosts/[serverId]/index" />
<Stack.Screen name="settings/hosts/[serverId]/[hostSection]" />
</Stack>
</ThemedStack>
);
}

View File

@@ -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 = (
<Stack screenOptions={HOST_STACK_SCREEN_OPTIONS}>
<ThemedStack screenOptions={HOST_STACK_SCREEN_OPTIONS}>
<Stack.Screen name="index" />
<Stack.Screen name="workspace/[workspaceId]/index" />
<Stack.Screen name="agent/[agentId]" options={AGENT_SCREEN_OPTIONS} />
<Stack.Screen name="sessions" />
<Stack.Screen name="open-project" />
<Stack.Screen name="settings" />
</Stack>
</ThemedStack>
);
if (!routeServerId) {

View File

@@ -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<NativeStackNavigationOptions>(
() => ({
...screenOptions,
contentStyle: [{ backgroundColor }, screenOptions?.contentStyle],
}),
[backgroundColor, screenOptions],
);
return <Stack screenOptions={themedScreenOptions}>{children}</Stack>;
}
export const ThemedStack = withUnistyles(ThemedStackBase, (theme) => ({
backgroundColor: theme.colors.surface0,
}));