From 60bc549835d504d56e1f933acc5e156a43a44a3c Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Tue, 4 Aug 2026 21:13:04 -0700 Subject: [PATCH 1/6] fix(mobile): restore Android tablet thread controls --- .../src/components/CompactBrandTitle.tsx | 2 ++ .../src/features/home/AndroidHomeFab.tsx | 4 +-- .../src/features/home/HomeRouteScreen.tsx | 25 +++++++++----- .../layout/AdaptiveWorkspaceLayout.tsx | 33 ++++++++++++------- .../features/threads/NewTaskDraftScreen.tsx | 16 ++++++--- .../threads/ThreadNavigationSidebar.tsx | 14 +++++--- .../sidebar-header-actions.android.tsx | 16 --------- 7 files changed, 61 insertions(+), 49 deletions(-) delete mode 100644 apps/mobile/src/features/threads/sidebar-header-actions.android.tsx diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx index 28f7cfe57a7..4ef61080b00 100644 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -57,6 +57,7 @@ export function CompactBrandTitle( > void; diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 7760920f7db..af2ae7d1b98 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -2,6 +2,7 @@ import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { useNavigation } from "@react-navigation/native"; import { useEffect, useMemo, useState } from "react"; +import { Platform } from "react-native"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { useProjects, useThreadShells } from "../../state/entities"; @@ -104,7 +105,11 @@ export function HomeRouteScreen() { return ( <> [] }} + options={ + Platform.OS === "android" + ? { headerShown: false } + : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] } + } /> navigation.navigate("NewTaskSheet", { screen: "NewTask" })} > <> - {/* Restore the compact title after the split branch blanks the detail - header. The brand slot doubles as the connection status surface: - while an environment reconnects, the lockup fades to a status label - in place (no layout shift in the list below). */} + {/* Restore the header after leaving split view; screen options are + shallow-merged. The brand slot also doubles as the connection + status surface while an environment reconnects. */} - navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" }), - })} + options={{ + ...getConnectionAwareBrandHeaderOptions({ + onOpenEnvironments: () => + navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" }), + }), + headerShown: true, + }} /> { + navigation.navigate("NewTaskSheet", { screen: "NewTask" }); + }, [navigation]); + // Minted here (root stack navigation) so the sidebar pane stays free of // navigation hooks — on iOS it renders inside an independent nav tree. const handleOpenEnvironmentSettings = useCallback(() => { @@ -522,18 +527,22 @@ function AdaptiveWorkspaceLayoutContent( pointerEvents={panes.primarySidebarVisible ? "auto" : "none"} style={sidebarAnimatedStyle} > - + + + + + ) : null} diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 6b121d85108..0ceedb692af 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -2,7 +2,11 @@ import { NativeStackScreenOptions } from "../../native/StackHeader"; import { StackActions, useNavigation, usePreventRemove } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, InteractionManager, Platform, View, useColorScheme } from "react-native"; -import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller"; +import { + KeyboardAvoidingView, + KeyboardStickyView, + useKeyboardState, +} from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; import { useFontFamily } from "../../lib/useFontFamily"; @@ -1053,9 +1057,11 @@ export function NewTaskDraftScreen(props: { navigation.goBack()} /> - - - + + ) : null} - + ); } diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index d80d906ada1..6f6413ef292 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -22,6 +22,7 @@ import type { SearchBarCommands } from "react-native-screens"; import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; +import { CompactBrandTitle } from "../../components/CompactBrandTitle"; import { ControlPillMenu } from "../../components/ControlPill"; import { SymbolView } from "../../components/AppSymbol"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; @@ -1282,7 +1283,10 @@ function ThreadNavigationSidebarPane( contentContainerStyle={[ styles.threadListContent, { - paddingBottom: 16 + insets.bottom, + paddingBottom: + Platform.OS === "android" + ? Math.max(insets.bottom, 16) + 88 + : 16 + insets.bottom, paddingTop: topListInset, }, ]} @@ -1338,16 +1342,16 @@ function ThreadNavigationSidebarPane( {/* Title slot doubles as the connection status surface: while an - environment reconnects, "Threads" fades to a status label in + environment reconnects, the brand fades to a status label in place (no layout shift in the list below). */} - Threads - + + + } /> diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.android.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.android.tsx deleted file mode 100644 index 1321c82c0d8..00000000000 --- a/apps/mobile/src/features/threads/sidebar-header-actions.android.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { View } from "react-native"; - -import { T3HeaderButton } from "../../native/T3HeaderButton.android"; -import type { SidebarHeaderActionsProps } from "./sidebar-header-actions"; - -export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { - return ( - - - - ); -} From 5cb0667bd2139fcc63d2ed14879a93a61ce3f302 Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Wed, 5 Aug 2026 20:29:15 -0700 Subject: [PATCH 2/6] fix(mobile): keep tablet thread header opaque --- .../threads/ThreadNavigationSidebar.tsx | 58 ++----------------- 1 file changed, 5 insertions(+), 53 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 6f6413ef292..ed8be674078 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -13,13 +13,12 @@ import { useAtomValue } from "@effect/atom-react"; import type { EnvironmentId } from "@t3tools/contracts"; import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; -import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; +import type { LayoutChangeEvent } from "react-native"; import { Platform, Pressable, StyleSheet, TextInput, View, useColorScheme } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { SearchBarCommands } from "react-native-screens"; -import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; import { CompactBrandTitle } from "../../components/CompactBrandTitle"; @@ -130,11 +129,6 @@ function SidebarHeaderButtonGroup(props: { } const SIDEBAR_STICKY_HEADER_HEIGHT = 106; -const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 44; -const SIDEBAR_HEADER_WASH_OPACITY = { - dark: [0.22, 0.14, 0.04], - light: [0.46, 0.3, 0.08], -} as const; interface ThreadNavigationSidebarProps { readonly width: number; @@ -196,11 +190,9 @@ function ThreadNavigationSidebarPane( const threads = useThreadShells(); const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); - const [headerIsOverContent, setHeaderIsOverContent] = useState(false); const searchInputRef = useRef(null); const searchBarRef = useRef(null); const openSwipeableRef = useRef(null); - const headerIsOverContentRef = useRef(false); const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); const { archiveThread, @@ -748,11 +740,10 @@ function ThreadNavigationSidebarPane( ); const backgroundColor = useThemeColor("--color-drawer"); + const headerBackgroundColor = useThemeColor("--color-screen"); const borderColor = useThemeColor("--color-border"); const mutedColor = useThemeColor("--color-foreground-muted"); const placeholderColor = useThemeColor("--color-placeholder"); - const headerFadeColor = String(backgroundColor); - const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(null); // The sticky header (title row, search field, optional connection status) // is measured so the list inset always matches its real height — no @@ -781,19 +772,10 @@ function ThreadNavigationSidebarPane( }, [props.onSelectThread], ); - const handleScroll = useCallback((event: NativeSyntheticEvent) => { - const next = event.nativeEvent.contentOffset.y > 6; - if (headerIsOverContentRef.current === next) { - return; - } - headerIsOverContentRef.current = next; - setHeaderIsOverContent(next); - }, []); const handleScrollBeginDrag = useCallback(() => { openSwipeableRef.current?.close(); }, []); const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ - onScroll: handleScroll, onScrollBeginDrag: handleScrollBeginDrag, }); // Project shells load after the first rows draw, so the maps they feed have @@ -1305,41 +1287,11 @@ function ThreadNavigationSidebarPane( - - - - - - - - - - - - - {/* Title slot doubles as the connection status surface: while an environment reconnects, the brand fades to a status label in @@ -1349,7 +1301,7 @@ function ThreadNavigationSidebarPane( onPress={props.onOpenEnvironmentSettings} size="pageTitle" brand={ - + } From d31911afe56a701e5b57ace44a32a9e9cc8f6cfe Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Wed, 5 Aug 2026 20:45:59 -0700 Subject: [PATCH 3/6] fix(mobile): suppress tablet header shadow --- .../src/features/threads/ThreadNavigationSidebar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index ed8be674078..88936892eff 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1290,7 +1290,12 @@ function ThreadNavigationSidebarPane( collapsable={false} onLayout={handleStickyHeaderLayout} pointerEvents="box-none" - style={{ paddingTop: insets.top, backgroundColor: headerBackgroundColor, elevation: 100 }} + style={{ + paddingTop: insets.top, + backgroundColor: headerBackgroundColor, + elevation: 100, + shadowColor: "transparent", + }} > {/* Title slot doubles as the connection status surface: while an From f90ba8246a127d2e727b112398df312648d5d58c Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Wed, 5 Aug 2026 20:57:30 -0700 Subject: [PATCH 4/6] fix(mobile): harden opaque tablet header --- apps/mobile/src/components/CompactBrandTitle.tsx | 5 +++-- .../src/features/threads/ThreadNavigationSidebar.tsx | 9 +++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx index 4ef61080b00..a4bf3c600f7 100644 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -33,6 +33,7 @@ export function brandTitleOffset(nativeLeadingItem: boolean): number { */ export function CompactBrandTitle( props: { + readonly allowFontScaling?: boolean; readonly nativeLeadingItem?: boolean; } = {}, ) { @@ -57,7 +58,7 @@ export function CompactBrandTitle( > @@ -1307,7 +1304,7 @@ function ThreadNavigationSidebarPane( size="pageTitle" brand={ - + } /> From 13590889a38020e38a6bf29e53543e1f41db783c Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Wed, 5 Aug 2026 21:02:36 -0700 Subject: [PATCH 5/6] fix(mobile): scope brand title scaling --- apps/mobile/src/components/CompactBrandTitle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx index a4bf3c600f7..bfba418c9fc 100644 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -94,7 +94,7 @@ export function CompactBrandTitle( } export function renderCompactBrandTitle() { - return ; + return ; } export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { From 211d96bc5c2ce2fba79b8b33aa59a101660e6d8c Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Fri, 7 Aug 2026 18:34:31 -0700 Subject: [PATCH 6/6] fix(mobile): avoid double-counting sidebar inset --- apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 39c821af940..8aaed0dc67c 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1266,7 +1266,7 @@ function ThreadNavigationSidebarPane( { paddingBottom: Platform.OS === "android" - ? Math.max(insets.bottom, 16) + 88 + ? Math.max(insets.bottom, 16) + 88 - insets.bottom : 16 + insets.bottom, paddingTop: topListInset, },