From e2248015aa849b7bee7242552ccb318364c5fc2e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 22 Mar 2026 20:12:01 +0700 Subject: [PATCH] refactor(sidebar): improve dropdown animations and spacing layout --- .../src/components/sidebar-workspace-list.tsx | 14 ++- .../app/src/components/ui/dropdown-menu.tsx | 90 +++++++++++++------ .../src/utils/sidebar-project-row-model.ts | 3 +- 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 6bcd150ae..00d52b249 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -1967,12 +1967,7 @@ const styles = StyleSheet.create((theme) => ({ projectBlock: { marginBottom: theme.spacing[1], }, - workspaceListContainer: { - marginLeft: theme.spacing[3], - paddingLeft: theme.spacing[2], - borderLeftWidth: StyleSheet.hairlineWidth, - borderLeftColor: theme.colors.surface1, - }, + workspaceListContainer: {}, emptyText: { color: theme.colors.foregroundMuted, textAlign: "center", @@ -1982,7 +1977,7 @@ const styles = StyleSheet.create((theme) => ({ projectRow: { minHeight: 36, paddingVertical: theme.spacing[2], - paddingHorizontal: theme.spacing[3], + paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius.lg, marginBottom: theme.spacing[1], flexDirection: "row", @@ -2051,6 +2046,7 @@ const styles = StyleSheet.create((theme) => ({ projectTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, + fontWeight: "400", minWidth: 0, flexShrink: 1, }, @@ -2108,7 +2104,8 @@ const styles = StyleSheet.create((theme) => ({ minHeight: 36, marginBottom: theme.spacing[1], paddingVertical: theme.spacing[2], - paddingHorizontal: theme.spacing[3], + paddingLeft: theme.spacing[3] + theme.spacing[3], + paddingRight: theme.spacing[3], borderRadius: theme.borderRadius.lg, flexDirection: "column", alignItems: "stretch", @@ -2195,6 +2192,7 @@ const styles = StyleSheet.create((theme) => ({ workspaceBranchText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, + fontWeight: "300", lineHeight: 20, opacity: 0.76, flex: 1, diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx index 7ba93ac83..0edc81cf7 100644 --- a/packages/app/src/components/ui/dropdown-menu.tsx +++ b/packages/app/src/components/ui/dropdown-menu.tsx @@ -24,7 +24,7 @@ import { type ViewStyle, type StyleProp, } from "react-native"; -import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; +import Animated, { Keyframe, runOnJS } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Check, CheckCircle } from "lucide-react-native"; @@ -226,6 +226,22 @@ export function DropdownMenuTrigger({ ); } +function getTransformOrigin(placement: Placement, alignment: Alignment): string { + const vertical = placement === "bottom" ? "top" : placement === "top" ? "bottom" : "center"; + const horizontal = alignment === "start" ? "left" : alignment === "end" ? "right" : "center"; + return `${vertical} ${horizontal}`; +} + +const contentEntering = new Keyframe({ + 0: { opacity: 0, transform: [{ scale: 0.97 }] }, + 100: { opacity: 1, transform: [{ scale: 1 }] }, +}).duration(150); + +const contentExiting = new Keyframe({ + 0: { opacity: 1, transform: [{ scale: 1 }] }, + 100: { opacity: 0, transform: [{ scale: 0.97 }] }, +}).duration(100); + export function DropdownMenuContent({ children, side = "bottom", @@ -249,9 +265,22 @@ export function DropdownMenuContent({ testID?: string; }>): ReactElement | null { const { open, setOpen, triggerRef } = useDropdownMenuContext("DropdownMenuContent"); + const [modalVisible, setModalVisible] = useState(false); + const [closing, setClosing] = useState(false); const [triggerRect, setTriggerRect] = useState(null); const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null); const [position, setPosition] = useState<{ x: number; y: number } | null>(null); + const [actualPlacement, setActualPlacement] = useState(side); + + // Keep Modal mounted during exit animation + useEffect(() => { + if (open) { + setModalVisible(true); + setClosing(false); + } else if (modalVisible) { + setClosing(true); + } + }, [open, modalVisible]); const handleClose = useCallback(() => { setOpen(false); @@ -314,6 +343,7 @@ export function DropdownMenuContent({ // For fullWidth, x is simply the horizontal padding to center on screen const x = fullWidth ? horizontalPadding : result.x; setPosition({ x, y: result.y }); + setActualPlacement(result.actualPlacement); }, [triggerRect, contentSize, side, align, offset, fullWidth, horizontalPadding]); const handleContentLayout = useCallback( @@ -324,7 +354,7 @@ export function DropdownMenuContent({ [], ); - if (!open) return null; + if (!modalVisible) return null; const { width: screenWidth } = Dimensions.get("window"); const resolvedWidthStyle: ViewStyle = fullWidth @@ -337,7 +367,7 @@ export function DropdownMenuContent({ return ( - - { + "worklet"; + if (finished) { + runOnJS(setModalVisible)(false); + } + })} + collapsable={false} + testID={testID} + onLayout={handleContentLayout} + style={[ + styles.content, + resolvedWidthStyle, + { + position: "absolute", + top: position?.y ?? -9999, + left: position?.x ?? -9999, + transformOrigin: getTransformOrigin(actualPlacement, align), + }, + ]} > - {children} - - + + {children} + + + ) : null} ); diff --git a/packages/app/src/utils/sidebar-project-row-model.ts b/packages/app/src/utils/sidebar-project-row-model.ts index 63924caf4..4f899d788 100644 --- a/packages/app/src/utils/sidebar-project-row-model.ts +++ b/packages/app/src/utils/sidebar-project-row-model.ts @@ -50,7 +50,8 @@ export function buildSidebarProjectRowModel(input: { }; } - const collapsible = input.project.workspaces.length > 1; + const collapsible = + input.project.projectKind === "git" || input.project.workspaces.length > 1; return { kind: "project_section",