diff --git a/frontend/src/resources/css/_chat.scss b/frontend/src/resources/css/_chat.scss index e687c1e..65cadf0 100644 --- a/frontend/src/resources/css/_chat.scss +++ b/frontend/src/resources/css/_chat.scss @@ -356,10 +356,34 @@ animation: fadeInDown 0.2s ease forwards; } + &.entering-left { + animation: fadeInLeft 0.2s ease forwards; + } + + &.entering-up { + animation: fadeInUp 0.2s ease forwards; + } + + &.entering-up-left { + animation: fadeInUpLeft 0.2s ease forwards; + } + &.closing { animation: fadeOutUp 0.2s ease forwards; } + &.closing-left { + animation: fadeOutRight 0.2s ease forwards; + } + + &.closing-up { + animation: fadeOutDown 0.2s ease forwards; + } + + &.closing-up-left { + animation: fadeOutDownRight 0.2s ease forwards; + } + .context-menu-item { display: flex; align-items: center; diff --git a/frontend/src/resources/css/common/_animations.scss b/frontend/src/resources/css/common/_animations.scss index 09904c8..d7711e2 100644 --- a/frontend/src/resources/css/common/_animations.scss +++ b/frontend/src/resources/css/common/_animations.scss @@ -38,6 +38,78 @@ } } +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(10px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUpLeft { + from { + opacity: 0; + transform: translate(10px, 10px); + } + + to { + opacity: 1; + transform: translate(0, 0); + } +} + +@keyframes fadeOutRight { + from { + opacity: 1; + transform: translateX(0); + } + + to { + opacity: 0; + transform: translateX(10px); + } +} + +@keyframes fadeOutDown { + from { + opacity: 1; + transform: translateY(0); + } + + to { + opacity: 0; + transform: translateY(10px); + } +} + +@keyframes fadeOutDownRight { + from { + opacity: 1; + transform: translate(0, 0); + } + + to { + opacity: 0; + transform: translate(10px, 10px); + } +} + .chat-switch-out { animation: fadeOutUp 0.2s ease forwards; } diff --git a/frontend/src/ui/components/chat/MessageContextMenu.tsx b/frontend/src/ui/components/chat/MessageContextMenu.tsx index 57e711d..50fb0f2 100644 --- a/frontend/src/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/ui/components/chat/MessageContextMenu.tsx @@ -36,6 +36,46 @@ export function MessageContextMenu({ const [editDialogOpen, setEditDialogOpen] = useState(false); const [replyDialogOpen, setReplyDialogOpen] = useState(false); const [isClosing, setIsClosing] = useState(false); + const [calculatedPosition, setCalculatedPosition] = useState(position); + const [animationClass, setAnimationClass] = useState('entering'); + + // Calculate smart positioning when component opens + useEffect(() => { + if (isOpen) { + const menuWidth = 160; // min-width from CSS + const menuHeight = isAuthor ? 120 : 60; // Approximate height based on items + const padding = 10; // Padding from viewport edges + + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let x = position.x; + let y = position.y; + let animation = 'entering'; + + // Check if menu would overflow right edge + if (x + menuWidth + padding > viewportWidth) { + x = viewportWidth - menuWidth - padding; + animation = 'entering-left'; // Animation from left side + } + + // Check if menu would overflow bottom edge + if (y + menuHeight + padding > viewportHeight) { + y = viewportHeight - menuHeight - padding; + animation = 'entering-up'; // Animation from bottom + } + + // If both edges would overflow, use top-left positioning + if (x + menuWidth + padding > viewportWidth && y + menuHeight + padding > viewportHeight) { + x = Math.max(padding, position.x - menuWidth); + y = Math.max(padding, position.y - menuHeight); + animation = 'entering-up-left'; + } + + setCalculatedPosition({ x, y }); + setAnimationClass(animation); + } + }, [isOpen, position, isAuthor]); // Effect to handle clicks outside the context menu useEffect(() => { @@ -97,10 +137,15 @@ export function MessageContextMenu({ const handleClose = () => { setIsClosing(true); + // Set appropriate closing animation based on opening animation + const closingAnimation = animationClass.replace('entering', 'closing'); + setAnimationClass(closingAnimation); + // Wait for animation to complete before calling onOpenChange setTimeout(() => { onOpenChange(false); setIsClosing(false); + setAnimationClass('entering'); // Reset for next opening }, 200); // Match the animation duration from _animations.scss }; @@ -122,12 +167,12 @@ export function MessageContextMenu({ const content = (
e.stopPropagation()}