diff --git a/apps/mobile/src/components/ui/AppBar.tsx b/apps/mobile/src/components/ui/AppBar.tsx index 7f76d9106613..2f593bcba163 100644 --- a/apps/mobile/src/components/ui/AppBar.tsx +++ b/apps/mobile/src/components/ui/AppBar.tsx @@ -10,7 +10,7 @@ import { NativeStackScreenOptions, useScreenOptionsWriter } from "../../native/S import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns"; import { useDeviceType } from "../../native/native-layout-metrics"; import { isDuoDevice } from "../../lib/device-type"; -import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout"; +import { useAdaptiveWorkspaceLayout } from "../../features/layout/adaptive-workspace-context"; import { useSidebarToggle } from "../../features/layout/useSidebarToggle"; import { InspectorCloseContext } from "../../features/layout/inspector-bar-store"; import { diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 19ffe072fb58..e9c6f4f6bab9 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -112,10 +112,11 @@ import { workspaceTabColumns, workspaceTabNavigationState, } from "./workspace/workspace-tab-navigation"; -import type { WorkspaceTab } from "./workspace/workspace-tabs-state"; +import { workspaceUsesColumns, type WorkspaceTab } from "./workspace/workspace-tabs-state"; import { workspaceRoutesOf } from "./workspace-overlay-routes"; import { SettingsWorkspaceLayout } from "../settings/SettingsWorkspaceLayout"; import { WorkspaceThreadSidebar } from "./workspace/WorkspaceThreadSidebar"; +import { WorkspaceSettingsSidebar } from "../settings/WorkspaceSettingsSidebar"; export { useAdaptiveWorkspaceLayout, useWorkspacePaneState }; @@ -263,11 +264,11 @@ function AdaptiveWorkspaceLayoutContent( const { width: viewportWidth, height } = nativeMetrics ?? windowDimensions; const pathname = props.pathname; const workspace = useWorkspaceTabs(); - // Settings and Usage occupy the workspace rather than a list/detail pair. + // Android workspace pages fill the viewport; legacy iPad pages retain their sidebar navigation. + // + // Android 工作区页面铺满窗口;旧版 iPad 保留侧栏导航,避免全屏内容与局部导航错位。 const androidSettingsOpen = useAndroidWorkspaceRoot()?.settingsOpen ?? false; const safeAreaInsets = useSafeAreaInsets(); - const workspacePage = - Platform.OS === "android" && (androidSettingsOpen || workspace?.state.selectedTab === "usage"); const navigation = useNavigation(); const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); const [fileInspectorPreferredVisible, setFileInspectorPreferredVisible] = useState(true); @@ -290,6 +291,18 @@ function AdaptiveWorkspaceLayoutContent( () => deriveLayout({ width: viewportWidth, height, nativeMetrics, nativeWorkspace }), [height, viewportWidth, nativeMetrics, nativeWorkspace], ); + const workspacePage = + (Platform.OS === "android" && + (androidSettingsOpen || workspace?.state.selectedTab === "usage")) || + (Platform.OS === "ios" && + !nativeWorkspace && + workspace !== null && + !workspaceUsesColumns( + workspace.state.selectedTab, + workspace.settingsOpen, + nativeWorkspace, + baseLayout.usesSplitView, + )); // The overlay sidebar closes when a page opens from it, and stays open on a list or on the // page a tab switch opened (`keepOpenFor`, set by that switch). const [overlaySidebar, setOverlaySidebar] = useState({ @@ -1010,8 +1023,15 @@ function AdaptiveWorkspaceLayoutContent( /> )} > - {/* Without a sidebar route (iPadOS before 26) the columns draw the thread list. */} - {sidebar ?? } + {/* The legacy sidebar owns its list while the root navigator owns detail pages. + + 兼容侧栏承载目录,详情仍由根导航管理,切换设置时不再隐藏整列。 */} + {sidebar ?? + (Platform.OS === "ios" && workspace?.settingsOpen ? ( + + ) : ( + + ))} {/* Keep the pill under the content throughout the sidebar's clipping animation. */} diff --git a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx index bac954d99f11..eaa212393538 100644 --- a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx +++ b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx @@ -522,7 +522,12 @@ function WorkspaceStackView(props: ViewProps & { readonly nativeColumns: boolean const descriptors = props.descriptors; const baseRoute = { key: `${props.state.key}:workspace`, name: "Workspace" }; const visibleRoute = projection.detail.at(-1) ?? primaryRoute; - const workspacePage = !workspaceUsesColumns(workspace.state.selectedTab, workspace.settingsOpen); + const workspacePage = !workspaceUsesColumns( + workspace.state.selectedTab, + workspace.settingsOpen, + props.nativeColumns, + layout.usesSplitView, + ); const baseDescriptor: Descriptor = { ...primaryDescriptor, navigation: props.descriptors[visibleRoute.key]!.navigation, @@ -649,8 +654,13 @@ function WorkspaceStackNavigator({ ); return ( - - + {/* The fallback sidebar is rendered by NavigationContent's layout, outside its children. + Share tab state above that layout so sidebar actions and detail tabs use one owner. + + 兼容侧栏由 NavigationContent 的布局绘制,位于其 children 外部; + 将标签状态提升到布局之上,使侧栏入口与详情导航共用同一份状态。 */} + + - - + + ); } diff --git a/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx b/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx index 2247ea50c9a4..3ea0e4b55616 100644 --- a/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx +++ b/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx @@ -3,7 +3,7 @@ import { NAVIGATION_TAB_ICONS, useEnabledNavigationTabs, } from "./navigation-tabs"; -import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; +import { use, useLayoutEffect, useRef, useState, type ReactNode } from "react"; import { View, useWindowDimensions, type NativeSyntheticEvent } from "react-native"; import { Tabs, type TabSelectedEvent, type TabSelectionRejectedEvent } from "react-native-screens"; @@ -16,6 +16,7 @@ import { } from "./workspace-tabs-state"; import { NativeWorkspaceTabActivityContext, + NativeWorkspaceModeContext, WorkspaceSlotOptionsContext, } from "../../../native/v5-workspace-context"; import { @@ -83,6 +84,7 @@ export function WorkspaceNativeTabs(props: { const selectTab = useSelectWorkspaceTab(); const [provenance, setProvenance] = useState(0); const systemTabBar = useSystemWorkspaceTabBar(); + const nativeColumns = use(NativeWorkspaceModeContext); const deviceType = useDeviceType(); const compactTabs = useCompactWorkspaceTabs(); const { layout } = useAdaptiveWorkspaceLayout(); @@ -91,7 +93,12 @@ export function WorkspaceNativeTabs(props: { const hiddenWithSidebar = deviceType === "pad" && layout.usesSplitView && - workspaceUsesColumns(workspace.state.selectedTab, workspace.settingsOpen) && + workspaceUsesColumns( + workspace.state.selectedTab, + workspace.settingsOpen, + nativeColumns, + layout.usesSplitView, + ) && !panes.primarySidebarVisible; const floatingTabs = useFloatingWorkspaceTabs(); const { width } = useWindowDimensions(); @@ -161,9 +168,17 @@ export function WorkspaceNativeTabs(props: { {(deviceType === "pad" || compactTabs || (layout.variant === "medium" && - !workspaceUsesColumns(selectedTab, workspace.settingsOpen))) && + !workspaceUsesColumns(selectedTab, workspace.settingsOpen, nativeColumns))) && !props.hidden && !props.tabsInSidebarColumn && + // The legacy split sidebar owns its own tabs; this host sits in the detail column. + // + // 兼容分栏的侧栏自行绘制导航,此容器位于右侧详情,不能重复绘制。 + !( + !nativeColumns && + layout.usesSplitView && + workspaceUsesColumns(selectedTab, workspace.settingsOpen, nativeColumns) + ) && !hiddenWithSidebar ? ( {props.children} + {/* Keep legacy iPad navigation in its sidebar; the detail host previously covered the composer. + + 旧版 iPad 的导航跟随侧栏,避免此前挂在详情容器上遮挡输入框。 */} + {deviceType === "pad" && workspace !== null ? : null} ); } diff --git a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts index 3f217fd70c8b..fea6688a690b 100644 --- a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts +++ b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts @@ -51,6 +51,18 @@ describe("workspace tab selections", () => { describe("opening a workspace tab", () => { const columns = { columns: true, check: available }; + it("opens the remembered Settings detail beside its list on legacy iPads", () => { + const next = workspaceTabNavigationState( + { routes: [home, thread] }, + "settings", + { settings: "SettingsAppearance" }, + { columns: workspaceTabColumns("ios", "settings", true), check: available }, + ); + expect(names(next)).toEqual(["Home", "Settings", "SettingsAppearance"]); + expect(workspaceTabColumns("ios", "threads", true)).toBe(true); + expect(workspaceTabColumns("ios", "settings", false)).toBe(false); + }); + it("opens Android Settings beside its default page only in a wide window", () => { const open = (wide: boolean) => workspaceTabNavigationState( diff --git a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts index 6a44e617e53e..082fbb50a700 100644 --- a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts +++ b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts @@ -22,6 +22,9 @@ export function workspaceTabColumns( tab: WorkspaceTab | "settings", columns: boolean, ) { + // Both iPad column implementations host Settings lists, so restore the detail beside either one. + // + // 两种 iPad 分栏都承载设置目录,恢复右侧设置页,避免兼容布局只显示全屏目录。 return columns && (platform === "ios" || (platform === "android" && tab === "settings")); } diff --git a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts index f1c839630eb3..c1e10fcd72e3 100644 --- a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts +++ b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts @@ -53,6 +53,16 @@ describe("Duo list clearance", () => { }); describe("workspace root navigation", () => { + it("keeps every legacy iPad tab in columns while retaining native Usage layout", () => { + expect(workspaceUsesColumns("threads", true, false)).toBe(true); + expect(workspaceUsesColumns("threads", true, true)).toBe(true); + expect(workspaceUsesColumns("usage", true, false)).toBe(true); + expect(workspaceUsesColumns("usage", true, true)).toBe(true); + expect(workspaceUsesColumns("threads", false, false)).toBe(true); + expect(workspaceUsesColumns("usage", false, false)).toBe(true); + expect(workspaceUsesColumns("usage", false, false, false)).toBe(false); + expect(workspaceUsesColumns("threads", true, false, false)).toBe(false); + }); it("pairs Settings, chats and pull requests with a detail column, and keeps Usage one page", () => { expect(workspaceUsesColumns("usage")).toBe(false); expect(workspaceUsesColumns("usage", true)).toBe(true); diff --git a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts index 3598894115dc..5a9c188443d5 100644 --- a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts +++ b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts @@ -16,9 +16,22 @@ export type WorkspaceTab = NavigationTab; export type WorkspaceSearchTab = WorkspaceTab | "settings"; export type WorkspaceSearchMode = "resting" | "active" | "expanded"; -/** Settings, Chats and pull requests pair a list with a detail column; Usage is one page. */ -export function workspaceUsesColumns(tab: WorkspaceTab, settingsOpen = false) { - return settingsOpen || tab === "threads" || tab === "pullRequests"; +/** Native Usage is one page; the legacy workspace keeps its sidebar for every tab. + * + * 原生分栏的用量为单页,兼容工作区的每个标签均保留侧栏。 + */ +export function workspaceUsesColumns( + tab: WorkspaceTab, + settingsOpen = false, + nativeColumns = true, + splitView = true, +) { + // Legacy iPad tabs retain their sidebar; full-page Settings and Usage stranded its navigation. + // + // 旧版 iPad 各标签保留侧栏,避免设置和用量铺满页面后导航仍滞留在左下角。 + return settingsOpen + ? nativeColumns || splitView + : (!nativeColumns && splitView) || tab === "threads" || tab === "pullRequests"; } export interface WorkspaceTabState { diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index a4177bfdd337..0c5d58ef6915 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -16,7 +16,7 @@ import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScroll import { EmptyState } from "../../components/ui/EmptyState"; import { useAuth, useUser } from "@clerk/expo"; import { useNavigation, useNavigationState } from "@react-navigation/native"; -import { use, type ComponentProps } from "react"; +import { use, type ComponentProps, type ReactNode } from "react"; import { Platform, View } from "react-native"; import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics"; import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-grouping"; @@ -65,13 +65,14 @@ function useSettingsListPlacement() { return { sidebar, selected }; } -function SettingsListContent() { +export function SettingsListContent() { return hasCloudPublicConfig() ? : ; } export function SettingsListScreen(props: { readonly sidebar: boolean; readonly selected: string | null; + readonly children?: ReactNode; }) { const workspace = useWorkspaceTabs(); const environmentMenu = useSettingsEnvironmentMenu(); @@ -93,7 +94,7 @@ export function SettingsListScreen(props: { : undefined } > - + {props.children ?? } diff --git a/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.test.tsx b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.test.tsx new file mode 100644 index 000000000000..3efb2b040c1e --- /dev/null +++ b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.test.tsx @@ -0,0 +1,145 @@ +import { act, type ContextType, type ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { describe, expect, it, vi } from "vite-plus/test"; +import { + NavigationContainerRefContext, + NavigationContext, + type NavigationState, +} from "@react-navigation/native"; + +vi.mock("@react-navigation/native", async () => { + // Resolve native's own core so the test exercises its real navigation-context fallback. + // + // 使用 native 实际依赖的 core,验证真实的导航上下文回退行为。 + const { createRequire } = await import("node:module"); + const require = createRequire(import.meta.url); + const core = createRequire(require.resolve("@react-navigation/native")).resolve( + "@react-navigation/core", + ); + const actual = await import(core); + return { + ...actual, + useNavigationState: (select: (state: NavigationState) => unknown) => + select(actual.useNavigation().getState()), + }; +}); +vi.mock("react-native", () => ({ + Platform: { + OS: "ios", + select: (values: { ios?: unknown; default?: unknown }) => values.ios ?? values.default, + }, + I18nManager: { isRTL: false }, + Linking: {}, +})); +vi.mock("../../components/ui/ListRow", () => ({ + ListRow: ({ label, onPress }: { label: string; onPress: () => void }) => ( + + ), +})); +vi.mock("../../components/ui/ListRowLayout", () => ({ ListRowLayout: () => null })); +vi.mock("../../components/ui/RowPressable", () => ({ RowPressable: () => null })); +vi.mock("./components/SettingsSection", async () => { + const { createContext } = await import("react"); + return { SettingsSidebarContext: createContext(null) }; +}); +vi.mock("./SettingsRouteScreen", async () => { + const { SettingsRow } = await import("./components/SettingsRow"); + return { + SettingsListScreen: ({ children }: { children: ReactNode }) => children, + SettingsListContent: () => ( + + ), + }; +}); +vi.mock("../layout/workspace/WorkspaceSidebarColumn", async () => { + const { NavigationContainerRefContext, NavigationContext } = + await import("@react-navigation/native"); + return { + WorkspaceSidebarColumn: ({ children }: { children: ReactNode }) => { + const standalone = { + getState: () => ({ + key: "standalone", + routes: [{ key: "standalone-route", name: "Standalone" }], + index: 0, + }), + getParent: () => undefined, + dispatch: () => {}, + }; + return ( + } + > + {children} + + ); + }, + }; +}); + +import { WorkspaceSettingsSidebar } from "./WorkspaceSettingsSidebar"; + +describe("legacy Settings sidebar navigation", () => { + it.each([false, true])( + "opens General in the workspace with a screen navigation context of %s", + (screenContext) => { + const state = { + key: "workspace", + type: "stack", + stale: false, + index: 2, + routeNames: ["Home", "Settings", "SettingsEnvironments", "SettingsOrganization"], + routes: [ + { key: "home", name: "Home" }, + { key: "settings", name: "Settings" }, + { key: "environments", name: "SettingsEnvironments" }, + ], + }; + const dispatch = vi.fn(); + const navigation = { + getState: () => state, + getRootState: () => state, + getParent: () => undefined, + addListener: () => () => {}, + dispatch, + }; + const container = document.createElement("div"); + const root = createRoot(container); + try { + act(() => + root.render( + } + > + ) + : undefined + } + > + + + , + ), + ); + act(() => container.querySelector("button")!.click()); + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: "RESET", + target: "workspace", + payload: expect.objectContaining({ + routeNames: state.routeNames, + routes: [ + state.routes[0], + state.routes[1], + { name: "SettingsOrganization", params: undefined }, + ], + }), + }), + ); + } finally { + act(() => root.unmount()); + } + }, + ); +}); diff --git a/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.tsx b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.tsx new file mode 100644 index 000000000000..8d830965c428 --- /dev/null +++ b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.tsx @@ -0,0 +1,35 @@ +import { use } from "react"; +import { + NavigationContainerRefContext, + NavigationContext, + NavigationRouteContext, + useNavigationState, +} from "@react-navigation/native"; + +import { WorkspaceSidebarColumn } from "../layout/workspace/WorkspaceSidebarColumn"; +import { SettingsListContent, SettingsListScreen } from "./SettingsRouteScreen"; + +export function WorkspaceSettingsSidebar(props: { readonly width: number }) { + const navigation = use(NavigationContext); + const container = use(NavigationContainerRefContext); + const state = useNavigationState((state) => state); + const index = state.routes.findIndex((route) => route.name === "Settings"); + const route = state.routes[index]; + if (!route) return null; + return ( + + + {/* Root layouts can lack a screen navigation object; preserve the container fallback too. + + 根布局可能没有页面导航对象;同时恢复容器回退,避免目录操作落到独立侧栏。 */} + + + + + + + + + + ); +} diff --git a/apps/mobile/src/features/settings/components/SettingsScreen.tsx b/apps/mobile/src/features/settings/components/SettingsScreen.tsx index 240ac009bdc5..2f74d0250e60 100644 --- a/apps/mobile/src/features/settings/components/SettingsScreen.tsx +++ b/apps/mobile/src/features/settings/components/SettingsScreen.tsx @@ -45,8 +45,12 @@ export function SettingsScreen( search={props.search} trailing={props.trailing} menus={props.menus} - onBack={workspaceRoot ? undefined : back} - guardBack={props.guardBack} + // Hide system Back as well as the callback, so the first detail cannot reveal a second list. + // + // 首个详情页同时隐藏系统返回并禁用回滑,避免右列退回后重复显示设置目录。 + onBack={workspaceRoot || back.columnRoot ? undefined : back.onBack} + onBackToList={!workspaceRoot && back.columnRoot ? back.onBack : undefined} + guardBack={props.guardBack || back.columnRoot} hideBottomBorder /> diff --git a/apps/mobile/src/features/settings/useSettingsPageBack.ts b/apps/mobile/src/features/settings/useSettingsPageBack.ts index 6ce909183410..e8df55db4809 100644 --- a/apps/mobile/src/features/settings/useSettingsPageBack.ts +++ b/apps/mobile/src/features/settings/useSettingsPageBack.ts @@ -5,9 +5,9 @@ import { resetAndroidWorkspace } from "../../lib/android-workspace-navigation"; import { useHardwareBack } from "../../lib/backHandler"; import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context"; -/** - * The page's Back action, or undefined for the first page beside the Settings list on wide - * Android: that page roots the detail column, so Back leaves Settings rather than emptying it. +/** The first Settings detail roots its column; nested pages can navigate back within it. + * + * 设置首个详情页为右列根页,后续子页才可在列内返回。 */ export function useSettingsPageBack() { const navigation = useNavigation(); @@ -15,11 +15,13 @@ export function useSettingsPageBack() { const { layout } = useAdaptiveWorkspaceLayout(); const routes = navigation.getState()?.routes ?? []; const below = routes[routes.findIndex((candidate) => candidate.key === route.key) - 1]; - const columnRoot = - Platform.OS === "android" && layout.usesSplitView && below?.name === "Settings"; - useHardwareBack(columnRoot, () => { + // The first detail page roots either split layout; Back must not duplicate the sidebar list. + // + // 两种分栏的首个设置页均为详情根页,避免返回后在右列重复显示左侧目录。 + const columnRoot = layout.usesSplitView && below?.name === "Settings"; + useHardwareBack(Platform.OS === "android" && columnRoot, () => { resetAndroidWorkspace(navigation); return true; }); - return columnRoot ? undefined : () => navigation.goBack(); + return { columnRoot, onBack: () => navigation.goBack() }; } diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 3d0253e2d3a2..9cb74429bef5 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -48,6 +48,8 @@ import { usePendingTaskListActions } from "../home/usePendingTaskListActions"; import { useThreadListActions } from "../home/useThreadListActions"; import { useFabScroll } from "../home/FabScrollContext"; import { WorkspaceListHeader } from "../layout/workspace/WorkspaceListHeader"; +import { useDeviceType } from "../../native/native-layout-metrics"; +import { workspaceDockBottomClearance } from "../layout/workspace/workspace-tabs-state"; import { WorkspaceSidebarColumn } from "../layout/workspace/WorkspaceSidebarColumn"; import { ThreadListV2PendingRow, @@ -110,6 +112,7 @@ export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) { function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { const floatingTabsEnabled = useAndroidFloatingTabs(); + const deviceType = useDeviceType(); const { themeVariables: materialTheme } = useAppearancePreferences(); const drawerColor = materialTheme["--color-drawer"]; @@ -597,7 +600,16 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { contentContainerStyle={[ styles.threadListContent, { - paddingBottom: Math.max(insets.bottom, 16) + 16, + // The iPad dock now belongs to this column; its last row must scroll above it. + // + // iPad 导航现归属此侧栏,末行需能滚动到导航上方,避免底部继续被遮挡。 + paddingBottom: + deviceType === "pad" + ? workspaceDockBottomClearance({ + bottomInset: insets.bottom, + keyboardHeight: 0, + }) + : Math.max(insets.bottom, 16) + 16, paddingTop: 6, }, ]} diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 046a2dc97b23..42bdfd799a00 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig, normalizePath } from "vite-plus"; import "vite-plus/test/config"; const reactTests = [ + "src/features/settings/WorkspaceSettingsSidebar.test.tsx", "src/features/threads/git/GitBranchesSheet.test.tsx", "src/features/threads/new-task-flow-provider.test.tsx", "src/features/layout/workspace/workspace-tabs-state.test.ts",