diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 968be6c14a8..382a7100757 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -26,6 +26,8 @@ import { useClerkSettingsSheetDetent, } from "../features/cloud/ClerkSettingsSheetDetent"; import { useAgentNotificationNavigation } from "../features/agent-awareness/notificationNavigation"; +import { pushScreenAnimation } from "../lib/pushScreenAnimation"; +import { useHeaderBlurEffect } from "../lib/useHeaderBlurEffect"; import { useThemeColor } from "../lib/useThemeColor"; function AppNavigator() { @@ -45,7 +47,9 @@ function AppNavigatorContent() { const { collapse, isExpanded } = useClerkSettingsSheetDetent(); const colorScheme = useColorScheme(); const statusBarBg = useThemeColor("--color-status-bar"); + const screenColor = useThemeColor("--color-screen"); const sheetStyle = useResolveClassNames("bg-sheet"); + const headerBlurEffect = useHeaderBlurEffect(); useAgentNotificationNavigation(); useThreadOutboxDrain(); @@ -96,9 +100,10 @@ function AppNavigatorContent() { @@ -112,9 +117,10 @@ function AppNavigatorContent() { diff --git a/apps/mobile/src/app/connections/_layout.tsx b/apps/mobile/src/app/connections/_layout.tsx index 902b53cb15a..4bf840a0b75 100644 --- a/apps/mobile/src/app/connections/_layout.tsx +++ b/apps/mobile/src/app/connections/_layout.tsx @@ -1,5 +1,6 @@ import Stack from "expo-router/stack"; import { useResolveClassNames } from "uniwind"; +import { pushScreenAnimation } from "../../lib/pushScreenAnimation"; import { useThemeColor } from "../../lib/useThemeColor"; export const unstable_settings = { @@ -23,7 +24,7 @@ export default function ConnectionsLayout() { }} > - + ); } diff --git a/apps/mobile/src/app/new/_layout.tsx b/apps/mobile/src/app/new/_layout.tsx index 2113b13311c..6960aaae2bc 100644 --- a/apps/mobile/src/app/new/_layout.tsx +++ b/apps/mobile/src/app/new/_layout.tsx @@ -2,6 +2,7 @@ import Stack from "expo-router/stack"; import { useResolveClassNames } from "uniwind"; import { NewTaskFlowProvider } from "../../features/threads/new-task-flow-provider"; +import { pushScreenAnimation } from "../../lib/pushScreenAnimation"; import { useThemeColor } from "../../lib/useThemeColor"; export const unstable_settings = { @@ -29,21 +30,24 @@ export default function NewTaskLayout() { + - ); diff --git a/apps/mobile/src/app/settings/_layout.tsx b/apps/mobile/src/app/settings/_layout.tsx index 2607c2cd1f1..193ba4b90a4 100644 --- a/apps/mobile/src/app/settings/_layout.tsx +++ b/apps/mobile/src/app/settings/_layout.tsx @@ -3,6 +3,7 @@ import { useCallback } from "react"; import { useResolveClassNames } from "uniwind"; import { useClerkSettingsSheetDetent } from "../../features/cloud/ClerkSettingsSheetDetent"; +import { pushScreenAnimation } from "../../lib/pushScreenAnimation"; import { useThemeColor } from "../../lib/useThemeColor"; export const unstable_settings = { @@ -37,25 +38,25 @@ export default function SettingsLayout() { ); diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx index 92e90920e2d..dcab393b216 100644 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +++ b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx @@ -2,7 +2,11 @@ import Stack from "expo-router/stack"; import { StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; +import { pushScreenAnimation } from "../../../../lib/pushScreenAnimation"; +import { useHeaderBlurEffect } from "../../../../lib/useHeaderBlurEffect"; + export default function ThreadLayout() { + const headerBlurEffect = useHeaderBlurEffect(); const sheetStyle = StyleSheet.flatten(useResolveClassNames("bg-sheet")); const headerBg = { backgroundColor: (sheetStyle as { backgroundColor?: string })?.backgroundColor, @@ -16,6 +20,7 @@ export default function ThreadLayout() { contentStyle: { backgroundColor: "transparent" }, headerShown: true, headerTransparent: true, + headerBlurEffect, headerShadowVisible: false, headerTitle: "", }} @@ -45,8 +50,9 @@ export default function ThreadLayout() { , diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 9757d5fbf91..18b0e78b2df 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -11,6 +11,7 @@ import { import { Stack } from "expo-router"; import { Text as RNText, View } from "react-native"; +import { useHeaderBlurEffect } from "../../lib/useHeaderBlurEffect"; import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import type { HomeProjectSortOrder } from "./homeThreadList"; @@ -72,6 +73,7 @@ export function HomeHeader(props: { readonly onOpenSettings: () => void; readonly onStartNewTask: () => void; }) { + const headerBlurEffect = useHeaderBlurEffect(); const iconColor = useThemeColor("--color-icon"); const mutedColor = useThemeColor("--color-foreground-muted"); const subtleColor = useThemeColor("--color-subtle"); @@ -88,6 +90,7 @@ export function HomeHeader(props: { headerShown: true, headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, + headerBlurEffect, headerShadowVisible: false, headerTintColor: iconColor, headerTitle: "", diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 92203c0ed4e..44bba904a80 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -19,6 +19,7 @@ import { AppText as Text } from "../../components/AppText"; import { environmentCatalog } from "../../connection/catalog"; import { useEnvironmentPresentation } from "../../state/presentation"; import { useAtomCommand } from "../../state/use-atom-command"; +import { useHeaderBlurEffect } from "../../lib/useHeaderBlurEffect"; import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; @@ -113,6 +114,7 @@ function ReviewSelectionActionBar(props: { export function ReviewSheet() { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); + const headerBlurEffect = useHeaderBlurEffect(); const headerForeground = String(useThemeColor("--color-foreground")); const headerMuted = String(useThemeColor("--color-foreground-muted")); const headerIcon = String(useThemeColor("--color-icon")); @@ -241,6 +243,7 @@ export function ReviewSheet() { Gesture.Pan() .enabled(!isSplitLayout) - .hitSlop({ left: 0, width: 40 }) + // Confine the drawer pan to the top-left corner where it completes + // (see the `event.y` check below). Covering the full left edge stole + // the touch from the native swipe-back gesture, leaving back + // navigation dead below the header. + .hitSlop({ left: 0, width: 40, top: 0, height: drawerGestureThreshold }) .activeOffsetX([10, 999]) .failOffsetY([-24, 24]) .onEnd((event) => { diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index f8c916974e5..c16460731ba 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -5,6 +5,7 @@ import { EnvironmentId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { Pressable, ScrollView, Text as RNText, View } from "react-native"; import { useWorkspaceState } from "../../state/workspace"; +import { useHeaderBlurEffect } from "../../lib/useHeaderBlurEffect"; import { useThemeColor } from "../../lib/useThemeColor"; import { useEnvironmentQuery } from "../../state/query"; import { dismissGitActionResult, useGitActionProgress } from "../../state/use-vcs-action-state"; @@ -104,6 +105,7 @@ export function ThreadRouteScreen() { const iconColor = String(useThemeColor("--color-icon")); const foregroundColor = String(useThemeColor("--color-foreground")); const secondaryFg = String(useThemeColor("--color-foreground-secondary")); + const headerBlurEffect = useHeaderBlurEffect(); /* ─── Git status for native header trigger ───────────────────────── */ const gitStatus = useEnvironmentQuery( @@ -326,6 +328,7 @@ export function ThreadRouteScreen() { headerShown: true, headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, + headerBlurEffect, headerShadowVisible: false, headerTintColor: iconColor, headerBackTitle: "", diff --git a/apps/mobile/src/lib/pushScreenAnimation.ts b/apps/mobile/src/lib/pushScreenAnimation.ts new file mode 100644 index 00000000000..cb953e7c2f5 --- /dev/null +++ b/apps/mobile/src/lib/pushScreenAnimation.ts @@ -0,0 +1,12 @@ +import { Platform } from "react-native"; + +/** + * Stack animation for pushed (non-sheet) screens. + * + * iOS keeps the default push animation: forcing `slide_from_right` switches + * react-native-screens to its custom swipe animator, which paints a black + * void behind the outgoing screen during interactive swipe-back. The native + * default is visually the same slide with proper parallax over the previous + * screen. Android has no interactive pop, so it keeps `slide_from_right`. + */ +export const pushScreenAnimation = Platform.OS === "ios" ? "default" : "slide_from_right"; diff --git a/apps/mobile/src/lib/useHeaderBlurEffect.ts b/apps/mobile/src/lib/useHeaderBlurEffect.ts new file mode 100644 index 00000000000..94d091479dc --- /dev/null +++ b/apps/mobile/src/lib/useHeaderBlurEffect.ts @@ -0,0 +1,15 @@ +import { useColorScheme } from "react-native"; + +/** + * Blur effect for transparent navigation headers. + * + * Recent iOS betas stopped drawing the implicit material behind transparent + * navigation bars, and the trait-adaptive `systemChromeMaterial` resolves to + * its light variant there even while the app renders in dark mode — so pick + * the light/dark variant explicitly from the app color scheme. + */ +export function useHeaderBlurEffect() { + return useColorScheme() === "dark" + ? ("systemChromeMaterialDark" as const) + : ("systemChromeMaterialLight" as const); +}