From 931d12c77b28832e698e1c65ce4af8e9adaa8726 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Thu, 6 Nov 2025 17:38:05 +0100
Subject: [PATCH] Add animations on widget buttons and on action buttons
(#15631)
Animated:
- Grip
- Trash can
- Action buttons
https://github.com/user-attachments/assets/1cac7a5e-2036-4308-9622-3b4809ae90a3
---
.../PageHeaderActionMenuButtons.tsx | 41 ++++++++++-
.../widgets/components/WidgetPlaceholder.tsx | 1 +
.../widgets/components/WidgetRenderer.tsx | 15 +++-
.../widget-card/components/WidgetCard.tsx | 18 ++---
.../components/WidgetCardHeader.tsx | 55 +++++++++-----
.../widget-card/components/WidgetGrip.tsx | 16 +++-
.../__stories__/WidgetCard.stories.tsx | 2 +
.../ui/layout/page/components/PageHeader.tsx | 73 ++++++++++---------
8 files changed, 152 insertions(+), 69 deletions(-)
diff --git a/packages/twenty-front/src/modules/action-menu/components/PageHeaderActionMenuButtons.tsx b/packages/twenty-front/src/modules/action-menu/components/PageHeaderActionMenuButtons.tsx
index b4a9ccf047..7f34f05e0e 100644
--- a/packages/twenty-front/src/modules/action-menu/components/PageHeaderActionMenuButtons.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/PageHeaderActionMenuButtons.tsx
@@ -1,13 +1,46 @@
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
-
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { motion } from 'framer-motion';
import { useContext } from 'react';
+
+const StyledActionContainer = styled(motion.div)`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
export const PageHeaderActionMenuButtons = () => {
const { actions } = useContext(ActionMenuContext);
+ const theme = useTheme();
const pinnedActions = actions.filter((entry) => entry.isPinned);
- return pinnedActions.map((action) => (
-
- ));
+ const actionsWithPositionForAnimation = pinnedActions.map(
+ (action, index) => ({
+ action,
+ position: pinnedActions.length - index - 1,
+ }),
+ );
+
+ return (
+ <>
+ {actionsWithPositionForAnimation.map(({ action, position }) => (
+
+
+
+ ))}
+ >
+ );
};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetPlaceholder.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetPlaceholder.tsx
index 1e8e9a98b1..7691b3cdea 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetPlaceholder.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetPlaceholder.tsx
@@ -51,6 +51,7 @@ export const WidgetPlaceholder = () => {
isDragging={false}
>
{
+ setIsHovered(true);
+ };
+
+ const handleMouseLeave = () => {
+ setIsHovered(false);
+ };
+
return (
{layoutMode !== 'canvas' && (
void;
+ onMouseLeave?: () => void;
};
const StyledWidgetCard = styled.div<{
@@ -61,12 +63,8 @@ const StyledWidgetCard = styled.div<{
!isEditing &&
css`
&:hover {
- cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.border.color.strong};
-
- .widget-card-remove-button {
- display: flex !important;
- }
+ cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
}
`}
@@ -101,12 +99,8 @@ const StyledWidgetCard = styled.div<{
!isEditing &&
css`
&:hover {
- cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.border.color.strong};
-
- .widget-card-remove-button {
- display: flex !important;
- }
+ cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
}
`}
@@ -142,6 +136,8 @@ export const WidgetCard = ({
isEditing,
isDragging,
className,
+ onMouseEnter,
+ onMouseLeave,
}: WidgetCardProps) => {
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
@@ -156,6 +152,8 @@ export const WidgetCard = ({
isEditing={isEditing}
isDragging={isDragging}
className={className}
+ onMouseEnter={onMouseEnter}
+ onMouseLeave={onMouseLeave}
>
{children}
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx
index 953a9856cb..3af5781772 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx
@@ -1,3 +1,4 @@
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { type ReactNode } from 'react';
@@ -5,9 +6,11 @@ import { IconTrash, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { WidgetGrip } from '@/page-layout/widgets/widget-card/components/WidgetGrip';
+import { AnimatePresence, motion } from 'framer-motion';
import { isDefined } from 'twenty-shared/utils';
export type WidgetCardHeaderProps = {
+ isWidgetCardHovered: boolean;
isInEditMode: boolean;
isEmpty?: boolean;
title: string;
@@ -39,11 +42,14 @@ const StyledRightContainer = styled.div`
gap: ${({ theme }) => theme.spacing(0.5)};
`;
-const StyledIconButton = styled(IconButton)`
- display: none;
+const StyledIconButtonContainer = styled(motion.div)`
+ display: flex;
+ align-items: center;
+ justify-content: center;
`;
export const WidgetCardHeader = ({
+ isWidgetCardHovered = false,
isEmpty = false,
isInEditMode = false,
title,
@@ -51,28 +57,43 @@ export const WidgetCardHeader = ({
forbiddenDisplay,
className,
}: WidgetCardHeaderProps) => {
+ const theme = useTheme();
+
return (
- {!isEmpty && isInEditMode && (
- e.stopPropagation()}
- />
- )}
+
+ {!isEmpty && isInEditMode && (
+ e.stopPropagation()}
+ />
+ )}
+
{isDefined(forbiddenDisplay) && forbiddenDisplay}
- {!isEmpty && isInEditMode && onRemove && (
-
- )}
+
+ {!isEmpty && isInEditMode && onRemove && isWidgetCardHovered && (
+
+
+
+ )}
+
);
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetGrip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetGrip.tsx
index 5005a47b87..b3a317c122 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetGrip.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetGrip.tsx
@@ -1,8 +1,9 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { motion } from 'framer-motion';
import { IconGripVertical } from 'twenty-ui/display';
-const StyledGripContainer = styled.div`
+const StyledGripContainer = styled(motion.div)`
width: 20px;
height: 20px;
display: flex;
@@ -32,7 +33,18 @@ export const WidgetGrip = ({ className, onClick }: WidgetGripProps) => {
const theme = useTheme();
return (
-
+
{}}
title="Widget name"
@@ -189,6 +190,7 @@ export const Catalog: CatalogStory = {
isInEditMode={!isReadMode}
onRemove={!isReadMode ? () => {} : undefined}
title="Widget name"
+ isWidgetCardHovered={args.state === 'Hover'}
/>
-
- {!isMobile && !isNavigationDrawerExpanded && (
-
- )}
- {hasClosePageButton && (
- onClosePage?.()}
- />
- )}
-
-
- {Icon && (
-
-
-
+
+
+
+ {!isMobile && !isNavigationDrawerExpanded && (
+
)}
- {title && (
-
- {typeof title === 'string' ? (
-
- ) : (
- title
- )}
-
+ {hasClosePageButton && (
+ onClosePage?.()}
+ />
)}
-
-
-
- {children}
-
-
+
+ {Icon && (
+
+
+
+ )}
+ {title && (
+
+ {typeof title === 'string' ? (
+
+ ) : (
+ title
+ )}
+
+ )}
+
+
+
+
+ {children}
+
+
+
);
};