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,