diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx index 81aaef9040..bcbcf5f379 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx @@ -1,6 +1,7 @@ import { PageLayoutGridLayoutDragSelector } from '@/page-layout/components/PageLayoutGridLayoutDragSelector'; import { PageLayoutGridOverlay } from '@/page-layout/components/PageLayoutGridOverlay'; import { PageLayoutGridResizeHandle } from '@/page-layout/components/PageLayoutGridResizeHandle'; +import { ReactGridLayoutCardWrapper } from '@/page-layout/components/ReactGridLayoutCardWrapper'; import { EMPTY_LAYOUT } from '@/page-layout/constants/EmptyLayout'; import { PAGE_LAYOUT_CONFIG, @@ -13,6 +14,7 @@ import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState'; +import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState'; import { addPendingPlaceholderToLayouts } from '@/page-layout/utils/addPendingPlaceholderToLayouts'; import { filterPendingPlaceholderFromLayouts } from '@/page-layout/utils/filterPendingPlaceholderFromLayouts'; import { prepareGridLayoutItemsWithPlaceholders } from '@/page-layout/utils/prepareGridLayoutItemsWithPlaceholders'; @@ -78,6 +80,10 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => { pageLayoutDraggingWidgetIdComponentState, ); + const setResizingWidgetId = useSetRecoilComponentState( + pageLayoutResizingWidgetIdComponentState, + ); + const { handleLayoutChange } = usePageLayoutHandleLayoutChange(); const handleLayoutChangeWithoutPendingPlaceholder = ( @@ -165,13 +171,19 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => { resizeHandle={ isPageLayoutInEditMode ? : undefined } - resizeHandles={['se']} + resizeHandles={['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw']} onDragStart={(_layout, _oldItem, newItem) => { setDraggingWidgetId(newItem.i); }} onDragStop={() => { setDraggingWidgetId(null); }} + onResizeStart={(_layout, _oldItem, newItem) => { + setResizingWidgetId(newItem.i); + }} + onResizeStop={() => { + setResizingWidgetId(null); + }} onLayoutChange={handleLayoutChangeWithoutPendingPlaceholder} onBreakpointChange={(newBreakpoint) => setPageLayoutCurrentBreakpoint( @@ -180,13 +192,13 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => { } > {gridLayoutItems.map((item) => ( -
+ {item.type === 'placeholder' ? ( ) : ( )} -
+ ))} diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridResizeHandle.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridResizeHandle.tsx index 39ef81f12d..14e93c1ede 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridResizeHandle.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridResizeHandle.tsx @@ -1,10 +1,19 @@ -import { useTheme } from '@emotion/react'; +import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { forwardRef } from 'react'; -import { IconRadiusBottomRight } from 'twenty-ui/display'; +import { + IconRadiusBottomLeft, + IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, +} from 'twenty-ui/display'; + +type WidgetHandleAxis = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'; +type WidgetHorizontalHandleAxis = 'n' | 's'; +type WidgetVerticalHandleAxis = 'e' | 'w'; type PageLayoutGridResizeHandleProps = { - handleAxis?: string; + handleAxis?: WidgetHandleAxis; onMouseDown?: React.MouseEventHandler; onMouseUp?: React.MouseEventHandler; onTouchEnd?: React.TouchEventHandler; @@ -12,49 +21,218 @@ type PageLayoutGridResizeHandleProps = { style?: React.CSSProperties; }; -const StyledIcon = styled(IconRadiusBottomRight)` - bottom: ${({ theme }) => theme.spacing(1)}; +const StyledBottomRightIcon = styled(IconRadiusBottomRight)` + color: transparent; cursor: nwse-resize; - position: absolute; - right: ${({ theme }) => theme.spacing(1)}; - color: ${({ theme }) => theme.border.color.strong}; - display: none; :hover { color: ${({ theme }) => theme.font.color.tertiary}; } `; +const StyledBottomLeftIcon = styled(IconRadiusBottomLeft)` + color: transparent; + cursor: nesw-resize; + + :hover { + color: ${({ theme }) => theme.font.color.tertiary}; + } +`; + +const StyledTopLeftIcon = styled(IconRadiusTopLeft)` + color: transparent; + cursor: nwse-resize; + + :hover { + color: ${({ theme }) => theme.font.color.tertiary}; + } +`; + +const StyledTopRightIcon = styled(IconRadiusTopRight)` + color: transparent; + cursor: nesw-resize; + + :hover { + color: ${({ theme }) => theme.font.color.tertiary}; + } +`; + +const StyledVerticalHandle = styled.div` + border-radius: ${({ theme }) => theme.border.radius.sm}; + height: ${({ theme }) => theme.spacing(5)}; + width: ${({ theme }) => theme.icon.stroke.lg}px; +`; + +const StyledHorizontalHandle = styled.div` + border-radius: ${({ theme }) => theme.border.radius.sm}; + height: ${({ theme }) => theme.icon.stroke.lg}px; + width: ${({ theme }) => theme.spacing(5)}; +`; + +const StyledHorizontalHandleWrapper = styled.div<{ + widgetHandleAxis: WidgetHorizontalHandleAxis; +}>` + border-radius: ${({ theme }) => theme.border.radius.sm}; + cursor: row-resize; + transform: ${({ widgetHandleAxis }) => + widgetHandleAxis === 'n' ? 'translateY(-50%)' : 'translateY(50%)'}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(2)}; + + :hover { + & > div { + background-color: ${({ theme }) => theme.font.color.tertiary}; + } + } +`; + +const StyledVerticalHandleWrapper = styled.div<{ + widgetHandleAxis: WidgetVerticalHandleAxis; +}>` + cursor: col-resize; + border-radius: ${({ theme }) => theme.border.radius.sm}; + transform: ${({ widgetHandleAxis }) => + widgetHandleAxis === 'w' ? 'translateX(-50%)' : 'translateX(50%)'}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(2)}; + + :hover { + & > div { + background-color: ${({ theme }) => theme.font.color.tertiary}; + } + } +`; + +const StyledResizeHandleWrapper = styled.div<{ + widgetHandleAxis?: WidgetHandleAxis; +}>` + position: absolute; + ${({ theme, widgetHandleAxis }) => { + if (widgetHandleAxis === 'w') { + return css` + left: ${theme.spacing(2)}; + top: 50%; + transform: translateY(-50%); + `; + } + if (widgetHandleAxis === 'e') { + return css` + right: ${theme.spacing(2)}; + top: 50%; + transform: translateY(-50%); + `; + } + if (widgetHandleAxis === 'n') { + return css` + top: ${theme.spacing(2)}; + left: 50%; + transform: translateX(-50%); + `; + } + if (widgetHandleAxis === 's') { + return css` + bottom: ${theme.spacing(2)}; + left: 50%; + transform: translateX(-50%); + `; + } + if (widgetHandleAxis === 'se') { + return css` + bottom: ${theme.spacing(0.5)}; + right: ${theme.spacing(0.5)}; + `; + } + if (widgetHandleAxis === 'sw') { + return css` + bottom: ${theme.spacing(0.5)}; + left: ${theme.spacing(0.5)}; + `; + } + if (widgetHandleAxis === 'ne') { + return css` + right: ${theme.spacing(0.5)}; + top: ${theme.spacing(0.5)}; + `; + } + if (widgetHandleAxis === 'nw') { + return css` + left: ${theme.spacing(0.5)}; + top: ${theme.spacing(0.5)}; + `; + } + }} +`; + +const isVerticalHandle = ( + widgetHandleAxis?: WidgetHandleAxis, +): widgetHandleAxis is WidgetVerticalHandleAxis => + widgetHandleAxis === 'w' || widgetHandleAxis === 'e'; + +const isHorizontalHandle = ( + widgetHandleAxis?: WidgetHandleAxis, +): widgetHandleAxis is WidgetHorizontalHandleAxis => + widgetHandleAxis === 'n' || widgetHandleAxis === 's'; + export const PageLayoutGridResizeHandle = forwardRef< HTMLDivElement, PageLayoutGridResizeHandleProps ->((props, ref) => { - const theme = useTheme(); - const { - handleAxis = 'se', - onMouseDown, - onMouseUp, - onTouchEnd, - className, - style, - } = props; +>( + ( + { + handleAxis: widgetHandleAxis, + onMouseDown, + onMouseUp, + onTouchEnd, + className, + style, + }, + ref, + ) => { + const theme = useTheme(); - if (handleAxis !== 'se') return null; - - return ( -
- -
- ); -}); + return ( + + {isVerticalHandle(widgetHandleAxis) && ( + + + + )} + {isHorizontalHandle(widgetHandleAxis) && ( + + + + )} + {widgetHandleAxis === 'ne' && ( + + )} + {widgetHandleAxis === 'nw' && ( + + )} + {widgetHandleAxis === 'se' && ( + + )} + {widgetHandleAxis === 'sw' && ( + + )} + + ); + }, +); diff --git a/packages/twenty-front/src/modules/page-layout/components/ReactGridLayoutCardWrapper.tsx b/packages/twenty-front/src/modules/page-layout/components/ReactGridLayoutCardWrapper.tsx new file mode 100644 index 0000000000..f9759a0c5b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/ReactGridLayoutCardWrapper.tsx @@ -0,0 +1,34 @@ +import { forwardRef } from 'react'; + +export const ReactGridLayoutCardWrapper = forwardRef< + HTMLDivElement, + { + key: string; + children: React.ReactNode; + style?: React.CSSProperties; + className?: string; + onMouseDown?: React.MouseEventHandler; + onMouseUp?: React.MouseEventHandler; + onMouseMove?: React.MouseEventHandler; + } +>( + ( + { children, key, className, style, onMouseDown, onMouseUp, onMouseMove }, + ref, + ) => { + return ( +
+ {children} +
+ ); + }, +); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutResizingWidgetIdComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutResizingWidgetIdComponentState.ts new file mode 100644 index 0000000000..f36868d25e --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutResizingWidgetIdComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutResizingWidgetIdComponentState = createComponentState< + string | null +>({ + key: 'pageLayoutResizingWidgetIdComponentState', + defaultValue: null, + componentInstanceContext: PageLayoutComponentInstanceContext, +}); 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 e01196316c..4fc28255bc 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 @@ -60,6 +60,7 @@ export const WidgetPlaceholder = () => { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index 94a95b765c..72c7ff5aee 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -5,6 +5,7 @@ import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWi import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState'; import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay'; import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer'; import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab'; @@ -35,6 +36,10 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { pageLayoutDraggingWidgetIdComponentState, ); + const resizingWidgetId = useRecoilComponentValue( + pageLayoutResizingWidgetIdComponentState, + ); + const currentlyEditingWidgetId = useRecoilComponentValue( pageLayoutEditingWidgetIdComponentState, ); @@ -43,6 +48,8 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { const isDragging = draggingWidgetId === widget.id; + const isResizing = resizingWidgetId === widget.id; + const { hasAccess, restriction } = useWidgetPermissions(widget); const { layoutMode } = usePageLayoutContentContext(); @@ -76,9 +83,10 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { return ( { void; onMouseLeave?: () => void; @@ -28,13 +29,14 @@ const StyledWidgetCard = styled.div<{ isPageLayoutInEditMode: boolean; isEditing: boolean; isDragging: boolean; + isResizing: boolean; }>` box-sizing: border-box; display: flex; flex-direction: column; + position: relative; height: 100%; width: 100%; - position: relative; ${({ theme, @@ -44,6 +46,7 @@ const StyledWidgetCard = styled.div<{ isEditing, isDragging, isInPinnedTab, + isResizing, onClick, }) => { if (layoutMode === 'canvas') { @@ -64,6 +67,7 @@ const StyledWidgetCard = styled.div<{ ${isPageLayoutInEditMode && !isDragging && !isEditing && + !isResizing && css` &:hover { border: 1px solid ${theme.border.color.strong}; @@ -101,6 +105,7 @@ const StyledWidgetCard = styled.div<{ ${isPageLayoutInEditMode && !isDragging && !isEditing && + !isResizing && css` &:hover { border: 1px solid ${theme.border.color.strong}; @@ -150,6 +155,7 @@ export const WidgetCard = ({ isEditing, isDragging, isInPinnedTab, + isResizing = false, className, onMouseEnter, onMouseLeave, @@ -167,6 +173,7 @@ export const WidgetCard = ({ isEditing={isEditing} isDragging={isDragging} isInPinnedTab={isInPinnedTab} + isResizing={isResizing} className={className} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} 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 3af5781772..9a5cb15f24 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 @@ -17,6 +17,7 @@ export type WidgetCardHeaderProps = { onRemove?: (e?: React.MouseEvent) => void; forbiddenDisplay?: ReactNode; className?: string; + isResizing?: boolean; }; const StyledWidgetCardHeader = styled.div` @@ -52,6 +53,7 @@ export const WidgetCardHeader = ({ isWidgetCardHovered = false, isEmpty = false, isInEditMode = false, + isResizing = false, title, onRemove, forbiddenDisplay, @@ -75,24 +77,28 @@ export const WidgetCardHeader = ({ {isDefined(forbiddenDisplay) && forbiddenDisplay} - {!isEmpty && isInEditMode && onRemove && isWidgetCardHovered && ( - - - - )} + {!isResizing && + !isEmpty && + isInEditMode && + onRemove && + isWidgetCardHovered && ( + + + + )} diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 628a48c3b0..8eaada12f1 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -278,7 +278,10 @@ export { IconPuzzle, IconPuzzle2, IconQuestionMark, + IconRadiusBottomLeft, IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, IconRefresh, IconRefreshAlert, IconRefreshDot, diff --git a/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts index 75657b4687..616a1ebf98 100644 --- a/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts +++ b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts @@ -2839,7 +2839,6 @@ import { IconMilkshake, IconMinimize, IconMinus, - IconMinusVertical, IconMist, IconMistOff, IconMobiledata, @@ -7023,7 +7022,6 @@ export const ALL_ICONS = { IconMilkshake, IconMinimize, IconMinus, - IconMinusVertical, IconMist, IconMistOff, IconMobiledata, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index f9dd04087a..beea76616e 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -343,7 +343,10 @@ export { IconPuzzle, IconPuzzle2, IconQuestionMark, + IconRadiusBottomLeft, IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, IconRefresh, IconRefreshAlert, IconRefreshDot,