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:
@@ -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
|
Navigation `getId`, and `getId` has broken Android native-stack/Fabric by
|
||||||
reordering an already-mounted workspace screen.
|
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
|
## Regression Shape
|
||||||
|
|
||||||
Pure helper tests are useful but not enough. The failure mode here is native
|
Pure helper tests are useful but not enough. The failure mode here is native
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
type StartupBlocker,
|
type StartupBlocker,
|
||||||
} from "@/navigation/host-runtime-bootstrap";
|
} from "@/navigation/host-runtime-bootstrap";
|
||||||
import { registerWorkspaceRouteNavigationRef } from "@/navigation/workspace-route-navigation";
|
import { registerWorkspaceRouteNavigationRef } from "@/navigation/workspace-route-navigation";
|
||||||
|
import { ThemedStack } from "@/navigation/themed-stack";
|
||||||
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
import { listenToDesktopEvent } from "@/desktop/electron/events";
|
import { listenToDesktopEvent } from "@/desktop/electron/events";
|
||||||
import { updateDesktopWindowControls } from "@/desktop/electron/window";
|
import { updateDesktopWindowControls } from "@/desktop/electron/window";
|
||||||
@@ -784,21 +785,15 @@ function FaviconStatusSync() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROOT_STACK_SCREEN_OPTIONS = {
|
||||||
|
headerShown: false,
|
||||||
|
animation: "none" as const,
|
||||||
|
};
|
||||||
|
|
||||||
function RootStack() {
|
function RootStack() {
|
||||||
const storeReady = useStoreReady();
|
const storeReady = useStoreReady();
|
||||||
const { theme } = useUnistyles();
|
|
||||||
const stackScreenOptions = useMemo(
|
|
||||||
() => ({
|
|
||||||
headerShown: false,
|
|
||||||
animation: "none" as const,
|
|
||||||
contentStyle: {
|
|
||||||
backgroundColor: theme.colors.surface0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[theme.colors.surface0],
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Stack screenOptions={stackScreenOptions}>
|
<ThemedStack screenOptions={ROOT_STACK_SCREEN_OPTIONS}>
|
||||||
<Stack.Screen name="index" />
|
<Stack.Screen name="index" />
|
||||||
<Stack.Protected guard={storeReady}>
|
<Stack.Protected guard={storeReady}>
|
||||||
<Stack.Screen name="welcome" />
|
<Stack.Screen name="welcome" />
|
||||||
@@ -815,7 +810,7 @@ function RootStack() {
|
|||||||
<Stack.Screen name="h/[serverId]" />
|
<Stack.Screen name="h/[serverId]" />
|
||||||
<Stack.Screen name="settings/hosts/[serverId]/index" />
|
<Stack.Screen name="settings/hosts/[serverId]/index" />
|
||||||
<Stack.Screen name="settings/hosts/[serverId]/[hostSection]" />
|
<Stack.Screen name="settings/hosts/[serverId]/[hostSection]" />
|
||||||
</Stack>
|
</ThemedStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Redirect, Stack, useLocalSearchParams } from "expo-router";
|
|||||||
import { useHostRuntimeBootstrapState } from "@/app/_layout";
|
import { useHostRuntimeBootstrapState } from "@/app/_layout";
|
||||||
import { HostRouteProvider } from "@/navigation/host-route-context";
|
import { HostRouteProvider } from "@/navigation/host-route-context";
|
||||||
import { resolveStartupRoute } from "@/navigation/host-runtime-bootstrap";
|
import { resolveStartupRoute } from "@/navigation/host-runtime-bootstrap";
|
||||||
|
import { ThemedStack } from "@/navigation/themed-stack";
|
||||||
import { useHostRegistryStatus, useHosts } from "@/runtime/host-runtime";
|
import { useHostRegistryStatus, useHosts } from "@/runtime/host-runtime";
|
||||||
|
|
||||||
const HOST_STACK_SCREEN_OPTIONS = {
|
const HOST_STACK_SCREEN_OPTIONS = {
|
||||||
@@ -33,14 +34,14 @@ function KnownHostRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stack = (
|
const stack = (
|
||||||
<Stack screenOptions={HOST_STACK_SCREEN_OPTIONS}>
|
<ThemedStack screenOptions={HOST_STACK_SCREEN_OPTIONS}>
|
||||||
<Stack.Screen name="index" />
|
<Stack.Screen name="index" />
|
||||||
<Stack.Screen name="workspace/[workspaceId]/index" />
|
<Stack.Screen name="workspace/[workspaceId]/index" />
|
||||||
<Stack.Screen name="agent/[agentId]" options={AGENT_SCREEN_OPTIONS} />
|
<Stack.Screen name="agent/[agentId]" options={AGENT_SCREEN_OPTIONS} />
|
||||||
<Stack.Screen name="sessions" />
|
<Stack.Screen name="sessions" />
|
||||||
<Stack.Screen name="open-project" />
|
<Stack.Screen name="open-project" />
|
||||||
<Stack.Screen name="settings" />
|
<Stack.Screen name="settings" />
|
||||||
</Stack>
|
</ThemedStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!routeServerId) {
|
if (!routeServerId) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user