Add resizable left sidebar on desktop
Mirror the right sidebar's resize pattern: Gesture.Pan() drag handle on the right edge, animated width, persisted to panel store.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import { View, Pressable, Text, Platform } from "react-native";
|
||||
import { View, Pressable, Text, Platform, useWindowDimensions } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
@@ -26,7 +26,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { usePanelStore, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH } from "@/stores/panel-store";
|
||||
import { SidebarWorkspaceList } from "./sidebar-workspace-list";
|
||||
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
|
||||
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
|
||||
@@ -48,7 +48,8 @@ import {
|
||||
} from "@/utils/host-routes";
|
||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||
|
||||
const DESKTOP_SIDEBAR_WIDTH = 320;
|
||||
const MIN_CHAT_WIDTH = 400;
|
||||
|
||||
type SidebarShortcutModel = ReturnType<typeof useSidebarShortcutModel>;
|
||||
type SidebarTheme = ReturnType<typeof useUnistyles>["theme"];
|
||||
|
||||
@@ -631,17 +632,55 @@ function DesktopSidebar({
|
||||
const newAgentKeys = useShortcutKeys("new-agent");
|
||||
const dragHandlers = useDesktopDragHandlers();
|
||||
const trafficLightPadding = useTrafficLightPadding();
|
||||
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
||||
const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth);
|
||||
const { width: viewportWidth } = useWindowDimensions();
|
||||
const hostStatusDotStyle = useMemo(
|
||||
() => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
|
||||
[activeHostStatusColor],
|
||||
);
|
||||
|
||||
const startWidthRef = useRef(sidebarWidth);
|
||||
const resizeWidth = useSharedValue(sidebarWidth);
|
||||
|
||||
useEffect(() => {
|
||||
resizeWidth.value = sidebarWidth;
|
||||
}, [sidebarWidth, resizeWidth]);
|
||||
|
||||
const resizeGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.hitSlop({ left: 8, right: 8, top: 0, bottom: 0 })
|
||||
.onStart(() => {
|
||||
startWidthRef.current = sidebarWidth;
|
||||
resizeWidth.value = sidebarWidth;
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Dragging right (positive translationX) increases width
|
||||
const newWidth = startWidthRef.current + event.translationX;
|
||||
const maxWidth = Math.max(
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_SIDEBAR_WIDTH, viewportWidth - MIN_CHAT_WIDTH),
|
||||
);
|
||||
const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(maxWidth, newWidth));
|
||||
resizeWidth.value = clampedWidth;
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(setSidebarWidth)(resizeWidth.value);
|
||||
}),
|
||||
[sidebarWidth, resizeWidth, setSidebarWidth, viewportWidth],
|
||||
);
|
||||
|
||||
const resizeAnimatedStyle = useAnimatedStyle(() => ({
|
||||
width: resizeWidth.value,
|
||||
}));
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
|
||||
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
|
||||
{trafficLightPadding.top > 0 ? (
|
||||
<View style={{ height: trafficLightPadding.top }} {...dragHandlers} />
|
||||
) : null}
|
||||
@@ -740,7 +779,14 @@ function DesktopSidebar({
|
||||
anchorRef={hostTriggerRef}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Resize handle - absolutely positioned over right border */}
|
||||
<GestureDetector gesture={resizeGesture}>
|
||||
<View
|
||||
style={[styles.resizeHandle, Platform.OS === "web" && ({ cursor: "col-resize" } as any)]}
|
||||
/>
|
||||
</GestureDetector>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -766,10 +812,19 @@ const styles = StyleSheet.create((theme) => ({
|
||||
overflow: "hidden",
|
||||
},
|
||||
desktopSidebar: {
|
||||
position: "relative",
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surfaceSidebar,
|
||||
},
|
||||
resizeHandle: {
|
||||
position: "absolute",
|
||||
right: -5,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 10,
|
||||
zIndex: 10,
|
||||
},
|
||||
sidebarHeader: {
|
||||
height: {
|
||||
xs: HEADER_INNER_HEIGHT_MOBILE,
|
||||
|
||||
@@ -42,6 +42,10 @@ export interface ExplorerCheckoutContext {
|
||||
isGit: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SIDEBAR_WIDTH = 320;
|
||||
export const MIN_SIDEBAR_WIDTH = 200;
|
||||
export const MAX_SIDEBAR_WIDTH = 600;
|
||||
|
||||
export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = 400;
|
||||
export const MIN_EXPLORER_SIDEBAR_WIDTH = 280;
|
||||
// Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint.
|
||||
@@ -62,6 +66,7 @@ interface PanelState {
|
||||
explorerTab: ExplorerTab;
|
||||
explorerTabByCheckout: Record<string, ExplorerTab>;
|
||||
activeExplorerCheckout: ExplorerCheckoutContext | null;
|
||||
sidebarWidth: number;
|
||||
explorerWidth: number;
|
||||
explorerSortOption: SortOption;
|
||||
explorerFilesSplitRatio: number;
|
||||
@@ -79,6 +84,7 @@ interface PanelState {
|
||||
setExplorerTabForCheckout: (params: ExplorerCheckoutContext & { tab: ExplorerTab }) => void;
|
||||
activateExplorerTabForCheckout: (checkout: ExplorerCheckoutContext) => void;
|
||||
setActiveExplorerCheckout: (checkout: ExplorerCheckoutContext | null) => void;
|
||||
setSidebarWidth: (width: number) => void;
|
||||
setExplorerWidth: (width: number) => void;
|
||||
setExplorerSortOption: (option: SortOption) => void;
|
||||
setExplorerFilesSplitRatio: (ratio: number) => void;
|
||||
@@ -91,6 +97,10 @@ function clampNumber(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function clampSidebarWidth(width: number): number {
|
||||
return clampNumber(width, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
|
||||
}
|
||||
|
||||
function clampWidth(width: number): number {
|
||||
return clampNumber(width, MIN_EXPLORER_SIDEBAR_WIDTH, MAX_EXPLORER_SIDEBAR_WIDTH);
|
||||
}
|
||||
@@ -129,6 +139,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
explorerTab: "changes",
|
||||
explorerTabByCheckout: {},
|
||||
activeExplorerCheckout: null,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
explorerSortOption: "name",
|
||||
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
@@ -244,6 +255,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
}
|
||||
return { activeExplorerCheckout: checkout };
|
||||
}),
|
||||
setSidebarWidth: (width) => set({ sidebarWidth: clampSidebarWidth(width) }),
|
||||
setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }),
|
||||
setExplorerSortOption: (option) => set({ explorerSortOption: option }),
|
||||
setExplorerFilesSplitRatio: (ratio) =>
|
||||
@@ -255,7 +267,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
}),
|
||||
{
|
||||
name: "panel-state",
|
||||
version: 5,
|
||||
version: 6,
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
migrate: (persistedState, version) => {
|
||||
const state = persistedState as Partial<PanelState> & Record<string, unknown>;
|
||||
@@ -310,6 +322,10 @@ export const usePanelStore = create<PanelState>()(
|
||||
state.explorerTabByCheckout = next;
|
||||
}
|
||||
|
||||
if (version < 6 || typeof state.sidebarWidth !== "number") {
|
||||
state.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
}
|
||||
|
||||
state.activeExplorerCheckout = null;
|
||||
|
||||
return state as PanelState;
|
||||
@@ -319,6 +335,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
desktop: state.desktop,
|
||||
explorerTab: state.explorerTab,
|
||||
explorerTabByCheckout: state.explorerTabByCheckout,
|
||||
sidebarWidth: state.sidebarWidth,
|
||||
explorerWidth: state.explorerWidth,
|
||||
explorerSortOption: state.explorerSortOption,
|
||||
explorerFilesSplitRatio: state.explorerFilesSplitRatio,
|
||||
|
||||
Reference in New Issue
Block a user