From bfbc14ce3cb5f2315bb9d82e462aea626195cf1a Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Thu, 2 Jul 2026 03:03:23 -0700 Subject: [PATCH 1/4] fix(mobile): restore header blur and reliable swipe-back on iOS beta Recent iOS betas stopped drawing the implicit blur behind transparent navigation bars, leaving white header text over raw scrolling content. Set headerBlurEffect: systemChromeMaterial explicitly on the three transparent headers (home, thread detail, thread index layout). Swipe-back was also unreliable on thread detail: the drawer-open pan gesture's hit slop covered the entire left edge, stealing touches from the native interactive-pop gesture below the header. Confine that pan to the top strip where it can actually complete, and enable fullScreenGestureEnabled on the thread, review, and files screens so back-swipe works from anywhere on the screen. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/app/_layout.tsx | 4 ++++ .../src/app/threads/[environmentId]/[threadId]/_layout.tsx | 6 ++++++ apps/mobile/src/features/home/HomeHeader.tsx | 3 +++ apps/mobile/src/features/threads/ThreadDetailScreen.tsx | 6 +++++- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 3 +++ 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 968be6c14a8..045489e389f 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -99,6 +99,9 @@ function AppNavigatorContent() { contentStyle: { backgroundColor: "transparent" }, headerShown: true, headerTransparent: true, + // Recent iOS betas no longer draw an implicit blur behind + // transparent navigation bars, so request one explicitly. + headerBlurEffect: "systemChromeMaterial", headerShadowVisible: false, }} /> @@ -115,6 +118,7 @@ function AppNavigatorContent() { animation: "slide_from_right", contentStyle: { backgroundColor: "transparent" }, gestureEnabled: true, + fullScreenGestureEnabled: true, headerShown: false, }} /> diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx index 92e90920e2d..1e36909c32c 100644 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +++ b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx @@ -16,6 +16,9 @@ export default function ThreadLayout() { contentStyle: { backgroundColor: "transparent" }, headerShown: true, headerTransparent: true, + // Recent iOS betas no longer draw an implicit blur behind + // transparent navigation bars, so request one explicitly. + headerBlurEffect: "systemChromeMaterial", headerShadowVisible: false, headerTitle: "", }} @@ -47,6 +50,7 @@ export default function ThreadLayout() { options={{ animation: "slide_from_right", contentStyle: sheetStyle, + fullScreenGestureEnabled: true, headerBackButtonDisplayMode: "minimal", headerShown: true, headerTitle: "Files changed", @@ -60,6 +64,7 @@ export default function ThreadLayout() { options={{ animation: "slide_from_right", contentStyle: sheetStyle, + fullScreenGestureEnabled: true, headerBackButtonDisplayMode: "minimal", headerShown: true, headerTitle: "Files", @@ -73,6 +78,7 @@ export default function ThreadLayout() { options={{ animation: "slide_from_right", contentStyle: sheetStyle, + fullScreenGestureEnabled: true, headerBackButtonDisplayMode: "minimal", headerShown: true, headerTitle: "File", diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 9757d5fbf91..0fab91c237f 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -88,6 +88,9 @@ export function HomeHeader(props: { headerShown: true, headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, + // Recent iOS betas no longer draw an implicit blur behind + // transparent navigation bars, so request one explicitly. + headerBlurEffect: "systemChromeMaterial", headerShadowVisible: false, headerTintColor: iconColor, headerTitle: "", diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 62d1bce1157..397ca28177d 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -253,7 +253,11 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread () => 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..390fa9bef32 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -326,6 +326,9 @@ export function ThreadRouteScreen() { headerShown: true, headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, + // Recent iOS betas no longer draw an implicit blur behind + // transparent navigation bars, so request one explicitly. + headerBlurEffect: "systemChromeMaterial", headerShadowVisible: false, headerTintColor: iconColor, headerBackTitle: "", From e03abb3a61a7ff7f03f35f565464a1b2ec022d43 Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Thu, 2 Jul 2026 03:33:17 -0700 Subject: [PATCH 2/4] fix(mobile): dark header blur and remove black void on swipe-back The trait-adaptive systemChromeMaterial blur resolves to its light variant on the new iOS beta even while the app renders dark, washing the header out white. Pick systemChromeMaterialLight/Dark explicitly from the app color scheme via a shared useHeaderBlurEffect hook. The black void revealed behind the outgoing screen during swipe-back came from the custom slide_from_right animator combined with transparent screen content. Keep the native default push animation on iOS (identical look, proper parallax with the previous screen visible underneath) and give stack screens an opaque themed background. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/app/_layout.tsx | 18 +++++++++------ .../[environmentId]/[threadId]/_layout.tsx | 22 ++++++++++++------- apps/mobile/src/features/home/HomeHeader.tsx | 6 ++--- .../features/threads/ThreadRouteScreen.tsx | 6 ++--- apps/mobile/src/lib/useHeaderBlurEffect.ts | 15 +++++++++++++ 5 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 apps/mobile/src/lib/useHeaderBlurEffect.ts diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 045489e389f..7a19cecff11 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -8,7 +8,7 @@ import { import { usePathname } from "expo-router"; import Stack from "expo-router/stack"; import { useCallback } from "react"; -import { StatusBar, useColorScheme } from "react-native"; +import { Platform, StatusBar, useColorScheme } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -26,6 +26,7 @@ import { useClerkSettingsSheetDetent, } from "../features/cloud/ClerkSettingsSheetDetent"; import { useAgentNotificationNavigation } from "../features/agent-awareness/notificationNavigation"; +import { useHeaderBlurEffect } from "../lib/useHeaderBlurEffect"; import { useThemeColor } from "../lib/useThemeColor"; function AppNavigator() { @@ -45,7 +46,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,12 +99,10 @@ function AppNavigatorContent() { @@ -115,8 +116,11 @@ function AppNavigatorContent() { void; readonly onStartNewTask: () => void; }) { + const headerBlurEffect = useHeaderBlurEffect(); const iconColor = useThemeColor("--color-icon"); const mutedColor = useThemeColor("--color-foreground-muted"); const subtleColor = useThemeColor("--color-subtle"); @@ -88,9 +90,7 @@ export function HomeHeader(props: { headerShown: true, headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, - // Recent iOS betas no longer draw an implicit blur behind - // transparent navigation bars, so request one explicitly. - headerBlurEffect: "systemChromeMaterial", + headerBlurEffect, headerShadowVisible: false, headerTintColor: iconColor, headerTitle: "", diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 390fa9bef32..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,9 +328,7 @@ export function ThreadRouteScreen() { headerShown: true, headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, - // Recent iOS betas no longer draw an implicit blur behind - // transparent navigation bars, so request one explicitly. - headerBlurEffect: "systemChromeMaterial", + headerBlurEffect, headerShadowVisible: false, headerTintColor: iconColor, headerBackTitle: "", 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); +} From d327c77e100addc10debea3bc50306bcac7c73a0 Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Thu, 2 Jul 2026 04:04:58 -0700 Subject: [PATCH 3/4] refactor(mobile): share push animation, cover missed transparent headers Review follow-ups: - Extract the iOS push-animation choice into lib/pushScreenAnimation and use it in every stack that pushed with slide_from_right (root, thread, git, settings, connections, new) so the black-void workaround cannot drift between files. - Apply useHeaderBlurEffect to the two transparent headers the first pass missed: the review sheet and the files tree screen. - Drop String() around ColorValue contentStyle backgrounds; an unresolved CSS variable would otherwise stringify to the invalid color "undefined" instead of a harmless no-op. - Document why the terminal screen intentionally omits fullScreenGestureEnabled. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/app/_layout.tsx | 12 +++++------- apps/mobile/src/app/connections/_layout.tsx | 3 ++- apps/mobile/src/app/new/_layout.tsx | 11 ++++++----- apps/mobile/src/app/settings/_layout.tsx | 11 ++++++----- .../[environmentId]/[threadId]/_layout.tsx | 19 +++++++++---------- .../[threadId]/git/_layout.tsx | 8 +++++--- .../features/files/ThreadFilesRouteScreen.tsx | 3 +++ .../src/features/review/ReviewSheet.tsx | 3 +++ apps/mobile/src/lib/pushScreenAnimation.ts | 12 ++++++++++++ 9 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 apps/mobile/src/lib/pushScreenAnimation.ts diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 7a19cecff11..382a7100757 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -8,7 +8,7 @@ import { import { usePathname } from "expo-router"; import Stack from "expo-router/stack"; import { useCallback } from "react"; -import { Platform, StatusBar, useColorScheme } from "react-native"; +import { StatusBar, useColorScheme } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -26,6 +26,7 @@ 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"; @@ -99,7 +100,7 @@ function AppNavigatorContent() { - + ); } diff --git a/apps/mobile/src/app/new/_layout.tsx b/apps/mobile/src/app/new/_layout.tsx index 2113b13311c..d41e6121cee 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,21 @@ 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 6252d7be98b..dcab393b216 100644 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +++ b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx @@ -1,14 +1,10 @@ import Stack from "expo-router/stack"; -import { Platform, StyleSheet } from "react-native"; +import { StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; +import { pushScreenAnimation } from "../../../../lib/pushScreenAnimation"; import { useHeaderBlurEffect } from "../../../../lib/useHeaderBlurEffect"; -// 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. -const pushAnimation = Platform.OS === "ios" ? ("default" as const) : ("slide_from_right" as const); - export default function ThreadLayout() { const headerBlurEffect = useHeaderBlurEffect(); const sheetStyle = StyleSheet.flatten(useResolveClassNames("bg-sheet")); @@ -54,7 +50,7 @@ export default function ThreadLayout() { , 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() { Date: Thu, 2 Jul 2026 04:11:16 -0700 Subject: [PATCH 4/4] style(mobile): fix formatting in new/_layout.tsx Co-Authored-By: Claude Fable 5 --- apps/mobile/src/app/new/_layout.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/app/new/_layout.tsx b/apps/mobile/src/app/new/_layout.tsx index d41e6121cee..6960aaae2bc 100644 --- a/apps/mobile/src/app/new/_layout.tsx +++ b/apps/mobile/src/app/new/_layout.tsx @@ -44,7 +44,10 @@ export default function NewTaskLayout() { name="add-project/local" options={{ animation: pushScreenAnimation, title: "Local folder" }} /> - + );