feat(app): highlight sidebar resize handles
This commit is contained in:
@@ -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");
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import Animated, { useAnimatedStyle, useSharedValue, runOnJS } from "react-native-reanimated";
|
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 { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { X } from "lucide-react-native";
|
import { X } from "lucide-react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window";
|
||||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||||
import { RetainedPanelActivity } from "@/components/retained-panel";
|
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 { buildWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
|
||||||
import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout";
|
import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout";
|
||||||
|
|
||||||
@@ -212,9 +212,11 @@ export function ExplorerSidebar({
|
|||||||
return (
|
return (
|
||||||
<Animated.View style={desktopSidebarStyle}>
|
<Animated.View style={desktopSidebarStyle}>
|
||||||
<View style={DESKTOP_SIDEBAR_BORDER_STYLE}>
|
<View style={DESKTOP_SIDEBAR_BORDER_STYLE}>
|
||||||
<GestureDetector gesture={resizeGesture}>
|
<SidebarResizeHandle
|
||||||
<View style={RESIZE_HANDLE_STYLE} />
|
edge="left"
|
||||||
</GestureDetector>
|
gesture={resizeGesture}
|
||||||
|
testID="explorer-sidebar-resize-handle"
|
||||||
|
/>
|
||||||
|
|
||||||
<ExplorerSidebarContent
|
<ExplorerSidebarContent
|
||||||
activeTab={explorerTab}
|
activeTab={explorerTab}
|
||||||
@@ -460,14 +462,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderLeftColor: theme.colors.border,
|
borderLeftColor: theme.colors.border,
|
||||||
backgroundColor: theme.colors.surfaceSidebar,
|
backgroundColor: theme.colors.surfaceSidebar,
|
||||||
},
|
},
|
||||||
resizeHandle: {
|
|
||||||
position: "absolute",
|
|
||||||
left: -5,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: 10,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
sidebarContent: {
|
sidebarContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
@@ -524,4 +518,3 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }];
|
const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }];
|
||||||
const RESIZE_HANDLE_STYLE = [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)];
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
type PressableStateCallbackType,
|
type PressableStateCallbackType,
|
||||||
} from "react-native";
|
} 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 Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
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 { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
|
||||||
import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu";
|
import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu";
|
||||||
import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu";
|
import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu";
|
||||||
|
import { SidebarResizeHandle } from "@/components/sidebar-resize-handle";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout";
|
import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { isWeb } from "@/constants/platform";
|
|
||||||
import { useOpenAddProject } from "@/hooks/use-open-add-project";
|
import { useOpenAddProject } from "@/hooks/use-open-add-project";
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { canCreateWorktreeForProjectKind } from "@/projects/host-projects";
|
import { canCreateWorktreeForProjectKind } from "@/projects/host-projects";
|
||||||
@@ -765,11 +765,6 @@ function DesktopSidebar({
|
|||||||
() => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome],
|
() => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome],
|
||||||
[ownsTopLeft],
|
[ownsTopLeft],
|
||||||
);
|
);
|
||||||
const resizeHandleStyle = useMemo(
|
|
||||||
() => [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
accessibilityElementsHidden={!active}
|
accessibilityElementsHidden={!active}
|
||||||
@@ -844,10 +839,11 @@ function DesktopSidebar({
|
|||||||
handleOpenHostSettings={handleOpenHostSettings}
|
handleOpenHostSettings={handleOpenHostSettings}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Resize handle - absolutely positioned over right border */}
|
<SidebarResizeHandle
|
||||||
<GestureDetector gesture={resizeGesture}>
|
edge="right"
|
||||||
<View style={resizeHandleStyle} />
|
gesture={resizeGesture}
|
||||||
</GestureDetector>
|
testID="left-sidebar-resize-handle"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
@@ -998,14 +994,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderRightColor: theme.colors.border,
|
borderRightColor: theme.colors.border,
|
||||||
backgroundColor: theme.colors.surfaceSidebar,
|
backgroundColor: theme.colors.surfaceSidebar,
|
||||||
},
|
},
|
||||||
resizeHandle: {
|
|
||||||
position: "absolute",
|
|
||||||
right: -5,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: 10,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
sidebarDragArea: {
|
sidebarDragArea: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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<ReturnType<typeof setTimeout> | 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 (
|
||||||
|
<GestureDetector gesture={gesture}>
|
||||||
|
<Pressable
|
||||||
|
testID={testID}
|
||||||
|
style={hitAreaStyle}
|
||||||
|
onHoverIn={handleHoverIn}
|
||||||
|
onHoverOut={handleHoverOut}
|
||||||
|
>
|
||||||
|
{highlighted ? (
|
||||||
|
<View pointerEvents="none" testID={`${testID}-highlight`} style={styles.highlight} />
|
||||||
|
) : null}
|
||||||
|
</Pressable>
|
||||||
|
</GestureDetector>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
Reference in New Issue
Block a user