mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
Collapsible resizers (#3330)
* Collapsible resizable divs Co-authored-by: Tim O'Farrell <tofarr@gmai.com> Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
@@ -83,19 +83,19 @@ function App(): JSX.Element {
|
|||||||
className="grow h-full min-h-0 min-w-0 px-3 pt-3"
|
className="grow h-full min-h-0 min-w-0 px-3 pt-3"
|
||||||
initialSize={500}
|
initialSize={500}
|
||||||
firstChild={<ChatInterface />}
|
firstChild={<ChatInterface />}
|
||||||
firstClassName="min-w-[500px] rounded-xl overflow-hidden border border-neutral-600"
|
firstClassName="rounded-xl overflow-hidden border border-neutral-600"
|
||||||
secondChild={
|
secondChild={
|
||||||
<Container
|
<Container
|
||||||
orientation={Orientation.VERTICAL}
|
orientation={Orientation.VERTICAL}
|
||||||
className="grow h-full min-h-0 min-w-0"
|
className="h-full min-h-0 min-w-0"
|
||||||
initialSize={window.innerHeight - 300}
|
initialSize={window.innerHeight - 300}
|
||||||
firstChild={<Workspace />}
|
firstChild={<Workspace />}
|
||||||
firstClassName="min-h-72 rounded-xl border border-neutral-600 bg-neutral-800 flex flex-col overflow-hidden"
|
firstClassName="rounded-xl border border-neutral-600 bg-neutral-800 flex flex-col overflow-hidden"
|
||||||
secondChild={<Terminal />}
|
secondChild={<Terminal />}
|
||||||
secondClassName="min-h-72 rounded-xl border border-neutral-600 bg-neutral-800"
|
secondClassName="rounded-xl border border-neutral-600 bg-neutral-800"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
secondClassName="flex flex-col overflow-hidden grow min-w-[500px]"
|
secondClassName="flex flex-col overflow-hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Controls setSettingOpen={onSettingsModalOpen} />
|
<Controls setSettingOpen={onSettingsModalOpen} />
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
VscChevronDown,
|
||||||
|
VscChevronLeft,
|
||||||
|
VscChevronRight,
|
||||||
|
VscChevronUp,
|
||||||
|
} from "react-icons/vsc";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import IconButton from "./IconButton";
|
||||||
|
|
||||||
export enum Orientation {
|
export enum Orientation {
|
||||||
HORIZONTAL = "horizontal",
|
HORIZONTAL = "horizontal",
|
||||||
VERTICAL = "vertical",
|
VERTICAL = "vertical",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Collapse {
|
||||||
|
COLLAPSED = "collapsed",
|
||||||
|
SPLIT = "split",
|
||||||
|
FILLED = "filled",
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerProps = {
|
type ContainerProps = {
|
||||||
firstChild: React.ReactNode;
|
firstChild: React.ReactNode;
|
||||||
firstClassName: string | undefined;
|
firstClassName: string | undefined;
|
||||||
@@ -28,30 +41,40 @@ export function Container({
|
|||||||
const [firstSize, setFirstSize] = useState<number>(initialSize);
|
const [firstSize, setFirstSize] = useState<number>(initialSize);
|
||||||
const [dividerPosition, setDividerPosition] = useState<number | null>(null);
|
const [dividerPosition, setDividerPosition] = useState<number | null>(null);
|
||||||
const firstRef = useRef<HTMLDivElement>(null);
|
const firstRef = useRef<HTMLDivElement>(null);
|
||||||
|
const secondRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [collapse, setCollapse] = useState<Collapse>(Collapse.SPLIT);
|
||||||
|
const isHorizontal = orientation === Orientation.HORIZONTAL;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dividerPosition == null || !firstRef.current) {
|
if (dividerPosition == null || !firstRef.current) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const getFirstSizeFromEvent = (e: MouseEvent) => {
|
const getFirstSizeFromEvent = (e: MouseEvent) => {
|
||||||
const position =
|
const position = isHorizontal ? e.clientX : e.clientY;
|
||||||
orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY;
|
|
||||||
return firstSize + position - dividerPosition;
|
return firstSize + position - dividerPosition;
|
||||||
};
|
};
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newFirstSize = getFirstSizeFromEvent(e);
|
const newFirstSize = `${getFirstSizeFromEvent(e)}px`;
|
||||||
const { current } = firstRef;
|
const { current } = firstRef;
|
||||||
if (current) {
|
if (current) {
|
||||||
if (orientation === Orientation.HORIZONTAL) {
|
if (isHorizontal) {
|
||||||
current.style.width = `${newFirstSize}px`;
|
current.style.width = newFirstSize;
|
||||||
|
current.style.minWidth = newFirstSize;
|
||||||
} else {
|
} else {
|
||||||
current.style.height = `${newFirstSize}px`;
|
current.style.height = newFirstSize;
|
||||||
|
current.style.minHeight = newFirstSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onMouseUp = (e: MouseEvent) => {
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (firstRef.current) {
|
||||||
|
firstRef.current.style.transition = "";
|
||||||
|
}
|
||||||
|
if (secondRef.current) {
|
||||||
|
secondRef.current.style.transition = "";
|
||||||
|
}
|
||||||
setFirstSize(getFirstSizeFromEvent(e));
|
setFirstSize(getFirstSizeFromEvent(e));
|
||||||
setDividerPosition(null);
|
setDividerPosition(null);
|
||||||
document.removeEventListener("mousemove", onMouseMove);
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
@@ -67,33 +90,102 @@ export function Container({
|
|||||||
|
|
||||||
const onMouseDown = (e: React.MouseEvent) => {
|
const onMouseDown = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const position =
|
if (firstRef.current) {
|
||||||
orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY;
|
firstRef.current.style.transition = "none";
|
||||||
|
}
|
||||||
|
if (secondRef.current) {
|
||||||
|
secondRef.current.style.transition = "none";
|
||||||
|
}
|
||||||
|
const position = isHorizontal ? e.clientX : e.clientY;
|
||||||
setDividerPosition(position);
|
setDividerPosition(position);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyleForFirst = () => {
|
const getStyleForFirst = () => {
|
||||||
if (orientation === Orientation.HORIZONTAL) {
|
const style: CSSProperties = { overflow: "hidden" };
|
||||||
return { width: `${firstSize}px` };
|
if (collapse === Collapse.COLLAPSED) {
|
||||||
|
style.opacity = 0;
|
||||||
|
style.width = 0;
|
||||||
|
style.minWidth = 0;
|
||||||
|
style.height = 0;
|
||||||
|
style.minHeight = 0;
|
||||||
|
} else if (collapse === Collapse.SPLIT) {
|
||||||
|
const firstSizePx = `${firstSize}px`;
|
||||||
|
if (isHorizontal) {
|
||||||
|
style.width = firstSizePx;
|
||||||
|
style.minWidth = firstSizePx;
|
||||||
|
} else {
|
||||||
|
style.height = firstSizePx;
|
||||||
|
style.minHeight = firstSizePx;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style.flexGrow = 1;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyleForSecond = () => {
|
||||||
|
const style: CSSProperties = { overflow: "hidden" };
|
||||||
|
if (collapse === Collapse.FILLED) {
|
||||||
|
style.opacity = 0;
|
||||||
|
style.width = 0;
|
||||||
|
style.minWidth = 0;
|
||||||
|
style.height = 0;
|
||||||
|
style.minHeight = 0;
|
||||||
|
} else if (collapse === Collapse.SPLIT) {
|
||||||
|
style.flexGrow = 1;
|
||||||
|
} else {
|
||||||
|
style.flexGrow = 1;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCollapse = () => {
|
||||||
|
if (collapse === Collapse.SPLIT) {
|
||||||
|
setCollapse(Collapse.COLLAPSED);
|
||||||
|
} else {
|
||||||
|
setCollapse(Collapse.SPLIT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExpand = () => {
|
||||||
|
if (collapse === Collapse.SPLIT) {
|
||||||
|
setCollapse(Collapse.FILLED);
|
||||||
|
} else {
|
||||||
|
setCollapse(Collapse.SPLIT);
|
||||||
}
|
}
|
||||||
return { height: `${firstSize}px` };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={twMerge("flex", !isHorizontal && "flex-col", className)}>
|
||||||
className={twMerge(
|
<div
|
||||||
`flex ${orientation === Orientation.HORIZONTAL ? "" : "flex-col"}`,
|
ref={firstRef}
|
||||||
className,
|
className={twMerge(firstClassName, "transition-all ease-soft-spring")}
|
||||||
)}
|
style={getStyleForFirst()}
|
||||||
>
|
>
|
||||||
<div ref={firstRef} className={firstClassName} style={getStyleForFirst()}>
|
|
||||||
{firstChild}
|
{firstChild}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`${orientation === Orientation.VERTICAL ? "cursor-ns-resize h-3" : "cursor-ew-resize w-3"} shrink-0`}
|
className={`${isHorizontal ? "cursor-ew-resize w-3 flex-col" : "cursor-ns-resize h-3 flex-row"} shrink-0 flex justify-center items-center`}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={collapse === Collapse.SPLIT ? onMouseDown : undefined}
|
||||||
/>
|
>
|
||||||
<div className={twMerge(secondClassName, "flex-1")}>{secondChild}</div>
|
<IconButton
|
||||||
|
icon={isHorizontal ? <VscChevronLeft /> : <VscChevronUp />}
|
||||||
|
ariaLabel="Collapse"
|
||||||
|
onClick={onCollapse}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={isHorizontal ? <VscChevronRight /> : <VscChevronDown />}
|
||||||
|
ariaLabel="Expand"
|
||||||
|
onClick={onExpand}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={secondRef}
|
||||||
|
className={twMerge(secondClassName, "transition-all ease-soft-spring")}
|
||||||
|
style={getStyleForSecond()}
|
||||||
|
>
|
||||||
|
{secondChild}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user