feat: add resizable embed component │ (#1401)

- Created reusable ResizableWrapper component
- Added drag-to-resize functionality for embeds
This commit is contained in:
Philip Okugbe
2025-07-25 00:23:14 +01:00
committed by GitHub
parent 32c7ecd9cf
commit ad5cf1e18b
4 changed files with 253 additions and 15 deletions
@@ -0,0 +1,96 @@
.wrapper {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 8px;
}
.resizing {
user-select: none;
cursor: ns-resize;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background: transparent;
}
.resizeHandleBottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 24px;
cursor: ns-resize;
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
touch-action: none;
-webkit-user-select: none;
user-select: none;
@mixin light {
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.05));
}
@mixin dark {
background: linear-gradient(
to bottom,
transparent,
rgba(255, 255, 255, 0.05)
);
}
&:hover {
@mixin light {
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));
}
@mixin dark {
background: linear-gradient(
to bottom,
transparent,
rgba(255, 255, 255, 0.1)
);
}
}
}
.wrapper:hover .resizeHandleBottom,
.resizing .resizeHandleBottom {
opacity: 1;
}
.resizeBar {
width: 50px;
height: 4px;
border-radius: 2px;
transition: background-color 0.2s ease;
@mixin light {
background-color: var(--mantine-color-gray-5);
}
@mixin dark {
background-color: var(--mantine-color-gray-6);
}
}
.resizeHandleBottom:hover .resizeBar,
.resizing .resizeBar {
@mixin light {
background-color: var(--mantine-color-gray-7);
}
@mixin dark {
background-color: var(--mantine-color-gray-4);
}
}
@@ -0,0 +1,112 @@
import React, { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import clsx from "clsx";
import classes from "./resizable-wrapper.module.css";
interface ResizableWrapperProps {
children: ReactNode;
initialHeight?: number;
minHeight?: number;
maxHeight?: number;
onResize?: (height: number) => void;
isEditable?: boolean;
className?: string;
showHandles?: "always" | "hover";
direction?: "vertical" | "horizontal" | "both";
}
export const ResizableWrapper: React.FC<ResizableWrapperProps> = ({
children,
initialHeight = 480,
minHeight = 200,
maxHeight = 1200,
onResize,
isEditable = true,
className,
showHandles = "hover",
direction = "vertical",
}) => {
const [resizeParams, setResizeParams] = useState<{
initialSize: number;
initialClientY: number;
initialClientX: number;
} | null>(null);
const [currentHeight, setCurrentHeight] = useState(initialHeight);
const [isHovered, setIsHovered] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!resizeParams) return;
const handleMouseMove = (e: MouseEvent) => {
if (!wrapperRef.current) return;
if (direction === "vertical" || direction === "both") {
const deltaY = e.clientY - resizeParams.initialClientY;
const newHeight = Math.min(
Math.max(resizeParams.initialSize + deltaY, minHeight),
maxHeight
);
setCurrentHeight(newHeight);
wrapperRef.current.style.height = `${newHeight}px`;
}
};
const handleMouseUp = () => {
setResizeParams(null);
if (onResize && currentHeight !== initialHeight) {
onResize(currentHeight);
}
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [resizeParams, currentHeight, initialHeight, onResize, minHeight, maxHeight, direction]);
const handleResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setResizeParams({
initialSize: currentHeight,
initialClientY: e.clientY,
initialClientX: e.clientX,
});
document.body.style.cursor = "ns-resize";
document.body.style.userSelect = "none";
}, [currentHeight]);
const shouldShowHandles =
isEditable &&
(showHandles === "always" || (showHandles === "hover" && (isHovered || resizeParams)));
return (
<div
ref={wrapperRef}
className={clsx(classes.wrapper, className, {
[classes.resizing]: !!resizeParams,
})}
style={{ height: currentHeight }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{children}
{!!resizeParams && <div className={classes.overlay} />}
{shouldShowHandles && direction === "vertical" && (
<div
className={classes.resizeHandleBottom}
onMouseDown={handleResizeStart}
>
<div className={classes.resizeBar} />
</div>
)}
</div>
);
};