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} + + + ); };