diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx
index 28f7cfe57a7..bfba418c9fc 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,6 +58,7 @@ export function CompactBrandTitle(
>
;
+ return ;
}
export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] {
diff --git a/apps/mobile/src/features/home/AndroidHomeFab.tsx b/apps/mobile/src/features/home/AndroidHomeFab.tsx
index 5c7d5a0b988..c57964fce4a 100644
--- a/apps/mobile/src/features/home/AndroidHomeFab.tsx
+++ b/apps/mobile/src/features/home/AndroidHomeFab.tsx
@@ -6,8 +6,8 @@ import { SymbolView } from "../../components/AppSymbol";
import { useThemeColor } from "../../lib/useThemeColor";
/**
- * Android-only wrapper that overlays a bottom-right new-task FAB on the home
- * screen. Other platforms render children unchanged.
+ * Android-only wrapper that overlays a bottom-right new-task FAB on a thread
+ * list. Other platforms render children unchanged.
*/
export function AndroidHomeFabLayout(props: {
readonly onStartNewTask: () => 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..8aaed0dc67c 100644
--- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
+++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
@@ -13,15 +13,15 @@ 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";
import { ControlPillMenu } from "../../components/ControlPill";
import { SymbolView } from "../../components/AppSymbol";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
@@ -129,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;
@@ -195,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,
@@ -750,8 +743,6 @@ function ThreadNavigationSidebarPane(
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
@@ -780,19 +771,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
@@ -1282,7 +1264,10 @@ function ThreadNavigationSidebarPane(
contentContainerStyle={[
styles.threadListContent,
{
- paddingBottom: 16 + insets.bottom,
+ paddingBottom:
+ Platform.OS === "android"
+ ? Math.max(insets.bottom, 16) + 88 - insets.bottom
+ : 16 + insets.bottom,
paddingTop: topListInset,
},
]}
@@ -1301,53 +1286,26 @@ 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 (
-
-
-
- );
-}