diff --git a/packages/app/e2e/sidebar-resize-handle.spec.ts b/packages/app/e2e/sidebar-resize-handle.spec.ts
new file mode 100644
index 000000000..eda7f2c81
--- /dev/null
+++ b/packages/app/e2e/sidebar-resize-handle.spec.ts
@@ -0,0 +1,27 @@
+import { expect, test, type Page } from "./fixtures";
+
+test.use({ viewport: { width: 1600, height: 900 } });
+
+async function expectBorderHighlight(page: Page, testID: string) {
+ const handle = page.getByTestId(testID);
+ await expect(handle).toBeVisible();
+ await expect(page.getByTestId(`${testID}-highlight`)).toHaveCount(0);
+
+ await handle.hover();
+ await expect(page.getByTestId(`${testID}-highlight`)).toHaveCount(0);
+
+ const highlight = page.getByTestId(`${testID}-highlight`);
+ await expect(highlight).toBeVisible();
+ await expect(highlight).toHaveCSS("width", "1px");
+ await expect(highlight).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
+}
+
+test("both sidebar borders highlight on hover", async ({ page, withWorkspace }) => {
+ const workspace = await withWorkspace({ prefix: "sidebar-resize-handle-" });
+ await workspace.navigateTo();
+
+ await expectBorderHighlight(page, "left-sidebar-resize-handle");
+
+ await page.getByTestId("workspace-explorer-toggle").first().click();
+ await expectBorderHighlight(page, "explorer-sidebar-resize-handle");
+});
diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx
index 0af8a0398..8b6fa0e49 100644
--- a/packages/app/src/components/explorer-sidebar.tsx
+++ b/packages/app/src/components/explorer-sidebar.tsx
@@ -8,7 +8,7 @@ import {
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, { useAnimatedStyle, useSharedValue, runOnJS } from "react-native-reanimated";
-import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import { Gesture } from "react-native-gesture-handler";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { X } from "lucide-react-native";
import { useTranslation } from "react-i18next";
@@ -33,7 +33,7 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { RetainedPanelActivity } from "@/components/retained-panel";
-import { isWeb } from "@/constants/platform";
+import { SidebarResizeHandle } from "@/components/sidebar-resize-handle";
import { buildWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout";
@@ -212,9 +212,11 @@ export function ExplorerSidebar({
return (
-
-
-
+
({
borderLeftColor: theme.colors.border,
backgroundColor: theme.colors.surfaceSidebar,
},
- resizeHandle: {
- position: "absolute",
- left: -5,
- top: 0,
- bottom: 0,
- width: 10,
- zIndex: 10,
- },
sidebarContent: {
flex: 1,
minHeight: 0,
@@ -524,4 +518,3 @@ const styles = StyleSheet.create((theme) => ({
}));
const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }];
-const RESIZE_HANDLE_STYLE = [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)];
diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx
index 4dbac08be..0e36f9c78 100644
--- a/packages/app/src/components/left-sidebar.tsx
+++ b/packages/app/src/components/left-sidebar.tsx
@@ -20,7 +20,7 @@ import {
View,
type PressableStateCallbackType,
} from "react-native";
-import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import { Gesture } from "react-native-gesture-handler";
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -30,10 +30,10 @@ import { HostPicker } from "@/components/hosts/host-picker";
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu";
import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu";
+import { SidebarResizeHandle } from "@/components/sidebar-resize-handle";
import { Shortcut } from "@/components/ui/shortcut";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout";
-import { isWeb } from "@/constants/platform";
import { useOpenAddProject } from "@/hooks/use-open-add-project";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { canCreateWorktreeForProjectKind } from "@/projects/host-projects";
@@ -765,11 +765,6 @@ function DesktopSidebar({
() => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome],
[ownsTopLeft],
);
- const resizeHandleStyle = useMemo(
- () => [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)],
- [],
- );
-
return (
- {/* Resize handle - absolutely positioned over right border */}
-
-
-
+
);
@@ -998,14 +994,6 @@ const styles = StyleSheet.create((theme) => ({
borderRightColor: theme.colors.border,
backgroundColor: theme.colors.surfaceSidebar,
},
- resizeHandle: {
- position: "absolute",
- right: -5,
- top: 0,
- bottom: 0,
- width: 10,
- zIndex: 10,
- },
sidebarDragArea: {
position: "relative",
},
diff --git a/packages/app/src/components/sidebar-resize-handle.tsx b/packages/app/src/components/sidebar-resize-handle.tsx
new file mode 100644
index 000000000..98f4e918b
--- /dev/null
+++ b/packages/app/src/components/sidebar-resize-handle.tsx
@@ -0,0 +1,89 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { Pressable, View } from "react-native";
+import { GestureDetector, type GestureType } from "react-native-gesture-handler";
+import { StyleSheet } from "react-native-unistyles";
+import { isWeb } from "@/constants/platform";
+
+interface SidebarResizeHandleProps {
+ edge: "left" | "right";
+ gesture: GestureType;
+ testID: string;
+}
+
+const HIGHLIGHT_DELAY_MS = 100;
+
+const webResizeCursorStyle = isWeb
+ ? ({
+ cursor: "col-resize",
+ } as object)
+ : null;
+
+export function SidebarResizeHandle({ edge, gesture, testID }: SidebarResizeHandleProps) {
+ const [highlighted, setHighlighted] = useState(false);
+ const highlightTimerRef = useRef | null>(null);
+ const hitAreaStyle = edge === "left" ? LEFT_HIT_AREA_STYLE : RIGHT_HIT_AREA_STYLE;
+
+ const cancelHighlightTimer = useCallback(() => {
+ if (highlightTimerRef.current === null) return;
+ clearTimeout(highlightTimerRef.current);
+ highlightTimerRef.current = null;
+ }, []);
+
+ const handleHoverIn = useCallback(() => {
+ cancelHighlightTimer();
+ highlightTimerRef.current = setTimeout(() => {
+ highlightTimerRef.current = null;
+ setHighlighted(true);
+ }, HIGHLIGHT_DELAY_MS);
+ }, [cancelHighlightTimer]);
+
+ const handleHoverOut = useCallback(() => {
+ cancelHighlightTimer();
+ setHighlighted(false);
+ }, [cancelHighlightTimer]);
+
+ useEffect(() => cancelHighlightTimer, [cancelHighlightTimer]);
+
+ return (
+
+
+ {highlighted ? (
+
+ ) : null}
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ hitArea: {
+ position: "absolute",
+ top: 0,
+ bottom: 0,
+ width: 10,
+ zIndex: 10,
+ },
+ leftEdge: {
+ left: -5,
+ },
+ rightEdge: {
+ right: -5,
+ },
+ highlight: {
+ position: "absolute",
+ top: 0,
+ bottom: 0,
+ left: 5,
+ width: 1,
+ backgroundColor: theme.colors.foreground,
+ opacity: 0.25,
+ },
+}));
+
+const LEFT_HIT_AREA_STYLE = [styles.hitArea, styles.leftEdge, webResizeCursorStyle];
+const RIGHT_HIT_AREA_STYLE = [styles.hitArea, styles.rightEdge, webResizeCursorStyle];