diff --git a/packages/app/e2e/browser/sidebar-workspace.spec.ts b/packages/app/e2e/browser/sidebar-workspace.spec.ts index 8071b3b4f3..85de01a376 100644 --- a/packages/app/e2e/browser/sidebar-workspace.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace.spec.ts @@ -504,6 +504,17 @@ test.describe("Mobile sidebar panelState transition", () => { await expectMobileAgentSidebarHidden(page); }); + test("places the close button beside the first nav row instead of over it", async ({ page }) => { + await gotoAppShell(page); + await openMobileAgentSidebar(page); + await expectMobileAgentSidebarVisible(page); + + const firstRow = await page.getByTestId("sidebar-global-new-workspace").boundingBox(); + const closeButton = await page.getByTestId("sidebar-close").boundingBox(); + if (!firstRow || !closeButton) throw new Error("first nav row and close button must render"); + expect(closeButton.x).toBeGreaterThanOrEqual(firstRow.x + firstRow.width); + }); + test("keeps a pinned workspace rendered while the retained sidebar is closed", async ({ page, }) => { diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 943c1edd27..58d0dee053 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -21,8 +21,14 @@ import { Gesture } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import { scheduleOnRN } from "react-native-worklets"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; +import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; +import { + IconButtonChromeProvider, + iconButtonChromeGlyphSize, + mutedIconColorMapping, +} from "@/components/ui/icon-button-chrome"; import { resolveDesktopSidebarWidth } from "@/components/desktop-sidebar-layout"; import { SIDEBAR_RESIZE_ACTIVATION_OFFSET, @@ -62,6 +68,7 @@ import { usePanelStore } from "@/stores/panel-store"; import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window"; import { useCloseAgentListGesture } from "@/mobile-panels/gestures"; import { MobilePanelOverlay } from "@/mobile-panels/presentation"; +import type { ShortcutKey } from "@/utils/format-shortcut"; import { buildSettingsAddHostRoute, buildSettingsRoute } from "@/utils/host-routes"; import { openHostOverview } from "@/navigation/settings-navigation"; import { @@ -74,6 +81,8 @@ import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton"; import { SidebarCalloutSlot } from "./sidebar-callout-slot"; import { SidebarWorkspaceList } from "./sidebar-workspace-list"; +const ThemedX = withUnistyles(X); + type SidebarTheme = ReturnType["theme"]; const DEV_BUILD_LABEL = process.env.EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL?.trim() || null; @@ -545,6 +554,11 @@ function MobileSidebar({ closeSidebar(); }, [closeSidebar]); + const closeButton = useMemo( + () => , + [labels.closeSidebar, closeSidebar], + ); + const mobileSidebarInsetStyle = useMemo( () => ({ paddingTop: insetsTop, @@ -562,30 +576,11 @@ function MobileSidebar({ > - - - - {({ hovered, pressed }) => ( - - )} - - + {isInitialLoad && !hasActiveHostFilter ? ( @@ -626,6 +621,34 @@ function MobileSidebar({ ); } +const NO_SHORTCUT: ShortcutKey[] = []; + +/** The compact header's icon button, so the sidebar's close reads like the header it covers. */ +function SidebarCloseButton({ label, onPress }: { label: string; onPress: () => void }) { + return ( + + + + + + ); +} + function DesktopSidebar({ theme, workspaceGroups, @@ -856,24 +879,8 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, minHeight: 0, }, - mobileCloseButtonRow: { - position: "absolute", - top: theme.spacing[3], - left: 0, - right: 0, - zIndex: 2, - alignItems: "flex-end", - }, mobileCloseButton: { - // The 16px X paints farther inside its 32px hit target than the 14px Settings2 glyph. - // This optical inset puts their painted right edges on the same sidebar rail. - marginRight: theme.spacing[2] + 1.5, - width: 32, - height: 32, - alignItems: "center", - justifyContent: "center", - borderRadius: theme.borderRadius.lg, - backgroundColor: theme.colors.surfaceSidebar, + marginRight: theme.spacing[2], }, desktopSidebarBorder: { borderRightWidth: 1, diff --git a/packages/app/src/components/sidebar/sidebar-nav-rows.tsx b/packages/app/src/components/sidebar/sidebar-nav-rows.tsx index 425280207e..a1481dcff9 100644 --- a/packages/app/src/components/sidebar/sidebar-nav-rows.tsx +++ b/packages/app/src/components/sidebar/sidebar-nav-rows.tsx @@ -1,8 +1,9 @@ import { router, usePathname } from "expo-router"; import { CalendarClock, History, Plus, Search } from "lucide-react-native"; -import { memo, useCallback, useMemo, useRef, type ComponentType } from "react"; +import { memo, useCallback, useMemo, useRef, type ComponentType, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { View, type StyleProp, type ViewStyle } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { PluginSidebarItem } from "@/plugins/sidebar-items"; @@ -30,37 +31,49 @@ interface SidebarNavRowProps { interface SidebarNavRowsProps extends SidebarNavRowProps { /** Style for the group wrapper, which the sidebar owns. */ style?: StyleProp; + /** Sits to the right of the first row, which shrinks to make room. */ + firstRowTrailing?: ReactNode; } /** * Top-level sidebar navigation, ordered and filtered by the user's * `sidebarNavItems` preference. Renders nothing — not even the bordered group - * wrapper — when every item is hidden. + * wrapper — when every item is hidden and there is no `firstRowTrailing`. */ -export function SidebarNavRows({ style, onBeforeNavigate }: SidebarNavRowsProps) { +export function SidebarNavRows({ style, onBeforeNavigate, firstRowTrailing }: SidebarNavRowsProps) { const { items } = useSidebarNavItems("header"); const visibleItems = useMemo(() => items.filter((item) => item.visible), [items]); const groupRef = useRef(null); - if (visibleItems.length === 0) return null; + if (visibleItems.length === 0 && !firstRowTrailing) return null; + + const [firstRow, ...restRows] = visibleItems.map((item) => { + if (item.kind === "plugin") { + return ( + + ); + } + const Row = BUILTIN_ROWS[item.id]; + return ; + }); return ( - {visibleItems.map((item) => { - if (item.kind === "plugin") { - return ( - - ); - } - const Row = BUILTIN_ROWS[item.id]; - return ; - })} + {firstRowTrailing ? ( + + {firstRow} + {firstRowTrailing} + + ) : ( + firstRow + )} + {restRows} ); } @@ -179,3 +192,14 @@ const BUILTIN_ROWS: Record