mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor(sidebar): improve dropdown animations and spacing layout
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<Rect | null>(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<Placement>(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 (
|
||||
<Modal
|
||||
visible={open}
|
||||
visible={modalVisible}
|
||||
transparent
|
||||
animationType="none"
|
||||
statusBarTranslucent={Platform.OS === "android"}
|
||||
@@ -351,30 +381,38 @@ export function DropdownMenuContent({
|
||||
onPress={handleClose}
|
||||
testID={testID ? `${testID}-backdrop` : undefined}
|
||||
/>
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(100)}
|
||||
exiting={FadeOut.duration(100)}
|
||||
collapsable={false}
|
||||
testID={testID}
|
||||
onLayout={handleContentLayout}
|
||||
style={[
|
||||
styles.content,
|
||||
resolvedWidthStyle,
|
||||
{
|
||||
position: "absolute",
|
||||
top: position?.y ?? -9999,
|
||||
left: position?.x ?? -9999,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ScrollView
|
||||
bounces={false}
|
||||
showsVerticalScrollIndicator
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
{!closing ? (
|
||||
<Animated.View
|
||||
entering={contentEntering}
|
||||
exiting={contentExiting.withCallback((finished) => {
|
||||
"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}
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
<ScrollView
|
||||
bounces={false}
|
||||
showsVerticalScrollIndicator
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
) : null}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user