refactor(sidebar): improve dropdown animations and spacing layout
This commit is contained in:
@@ -1967,12 +1967,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
projectBlock: {
|
projectBlock: {
|
||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[1],
|
||||||
},
|
},
|
||||||
workspaceListContainer: {
|
workspaceListContainer: {},
|
||||||
marginLeft: theme.spacing[3],
|
|
||||||
paddingLeft: theme.spacing[2],
|
|
||||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
|
||||||
borderLeftColor: theme.colors.surface1,
|
|
||||||
},
|
|
||||||
emptyText: {
|
emptyText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
@@ -1982,7 +1977,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
projectRow: {
|
projectRow: {
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
paddingVertical: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[2],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[1],
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -2051,6 +2046,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
projectTitle: {
|
projectTitle: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: "400",
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
@@ -2108,7 +2104,8 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[1],
|
||||||
paddingVertical: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingLeft: theme.spacing[3] + theme.spacing[3],
|
||||||
|
paddingRight: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
@@ -2195,6 +2192,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
workspaceBranchText: {
|
workspaceBranchText: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: "300",
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
opacity: 0.76,
|
opacity: 0.76,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
type StyleProp,
|
type StyleProp,
|
||||||
} from "react-native";
|
} 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 { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Check, CheckCircle } from "lucide-react-native";
|
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({
|
export function DropdownMenuContent({
|
||||||
children,
|
children,
|
||||||
side = "bottom",
|
side = "bottom",
|
||||||
@@ -249,9 +265,22 @@ export function DropdownMenuContent({
|
|||||||
testID?: string;
|
testID?: string;
|
||||||
}>): ReactElement | null {
|
}>): ReactElement | null {
|
||||||
const { open, setOpen, triggerRef } = useDropdownMenuContext("DropdownMenuContent");
|
const { open, setOpen, triggerRef } = useDropdownMenuContext("DropdownMenuContent");
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [closing, setClosing] = useState(false);
|
||||||
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
||||||
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
||||||
const [position, setPosition] = useState<{ x: number; y: 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(() => {
|
const handleClose = useCallback(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -314,6 +343,7 @@ export function DropdownMenuContent({
|
|||||||
// For fullWidth, x is simply the horizontal padding to center on screen
|
// For fullWidth, x is simply the horizontal padding to center on screen
|
||||||
const x = fullWidth ? horizontalPadding : result.x;
|
const x = fullWidth ? horizontalPadding : result.x;
|
||||||
setPosition({ x, y: result.y });
|
setPosition({ x, y: result.y });
|
||||||
|
setActualPlacement(result.actualPlacement);
|
||||||
}, [triggerRect, contentSize, side, align, offset, fullWidth, horizontalPadding]);
|
}, [triggerRect, contentSize, side, align, offset, fullWidth, horizontalPadding]);
|
||||||
|
|
||||||
const handleContentLayout = useCallback(
|
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 { width: screenWidth } = Dimensions.get("window");
|
||||||
const resolvedWidthStyle: ViewStyle = fullWidth
|
const resolvedWidthStyle: ViewStyle = fullWidth
|
||||||
@@ -337,7 +367,7 @@ export function DropdownMenuContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={open}
|
visible={modalVisible}
|
||||||
transparent
|
transparent
|
||||||
animationType="none"
|
animationType="none"
|
||||||
statusBarTranslucent={Platform.OS === "android"}
|
statusBarTranslucent={Platform.OS === "android"}
|
||||||
@@ -351,30 +381,38 @@ export function DropdownMenuContent({
|
|||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
testID={testID ? `${testID}-backdrop` : undefined}
|
testID={testID ? `${testID}-backdrop` : undefined}
|
||||||
/>
|
/>
|
||||||
<Animated.View
|
{!closing ? (
|
||||||
entering={FadeIn.duration(100)}
|
<Animated.View
|
||||||
exiting={FadeOut.duration(100)}
|
entering={contentEntering}
|
||||||
collapsable={false}
|
exiting={contentExiting.withCallback((finished) => {
|
||||||
testID={testID}
|
"worklet";
|
||||||
onLayout={handleContentLayout}
|
if (finished) {
|
||||||
style={[
|
runOnJS(setModalVisible)(false);
|
||||||
styles.content,
|
}
|
||||||
resolvedWidthStyle,
|
})}
|
||||||
{
|
collapsable={false}
|
||||||
position: "absolute",
|
testID={testID}
|
||||||
top: position?.y ?? -9999,
|
onLayout={handleContentLayout}
|
||||||
left: position?.x ?? -9999,
|
style={[
|
||||||
},
|
styles.content,
|
||||||
]}
|
resolvedWidthStyle,
|
||||||
>
|
{
|
||||||
<ScrollView
|
position: "absolute",
|
||||||
bounces={false}
|
top: position?.y ?? -9999,
|
||||||
showsVerticalScrollIndicator
|
left: position?.x ?? -9999,
|
||||||
contentContainerStyle={{ flexGrow: 1 }}
|
transformOrigin: getTransformOrigin(actualPlacement, align),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{children}
|
<ScrollView
|
||||||
</ScrollView>
|
bounces={false}
|
||||||
</Animated.View>
|
showsVerticalScrollIndicator
|
||||||
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollView>
|
||||||
|
</Animated.View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</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 {
|
return {
|
||||||
kind: "project_section",
|
kind: "project_section",
|
||||||
|
|||||||
Reference in New Issue
Block a user