From 5e3a59d5a297a8fc5589a9935ddf08db5c7e512c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:24:23 -0600 Subject: [PATCH] feat: resize handle --- packages/desktop/src/pages/layout.tsx | 44 +++--------- packages/desktop/src/pages/session.tsx | 44 +++--------- packages/ui/src/components/resize-handle.css | 20 ++++++ packages/ui/src/components/resize-handle.tsx | 71 ++++++++++++++++++++ packages/ui/src/styles/index.css | 1 + 5 files changed, 110 insertions(+), 70 deletions(-) create mode 100644 packages/ui/src/components/resize-handle.css create mode 100644 packages/ui/src/components/resize-handle.tsx diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 0c0d99147..166ee7beb 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -5,6 +5,7 @@ import { useLayout } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { Mark } from "@opencode-ai/ui/logo" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -116,41 +117,14 @@ export default function Layout(props: ParentProps) { style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }} > -
{ - e.preventDefault() - const startX = e.clientX - const startWidth = layout.sidebar.width() - const maxWidth = window.innerWidth * 0.3 - const minWidth = 150 - const collapseThreshold = 80 - let currentWidth = startWidth - - document.body.style.userSelect = "none" - document.body.style.overflow = "hidden" - - const onMouseMove = (moveEvent: MouseEvent) => { - const deltaX = moveEvent.clientX - startX - currentWidth = startWidth + deltaX - const clampedWidth = Math.min(maxWidth, Math.max(minWidth, currentWidth)) - layout.sidebar.resize(clampedWidth) - } - - const onMouseUp = () => { - document.body.style.userSelect = "" - document.body.style.overflow = "" - document.removeEventListener("mousemove", onMouseMove) - document.removeEventListener("mouseup", onMouseUp) - - if (currentWidth < collapseThreshold) { - layout.sidebar.close() - } - } - - document.addEventListener("mousemove", onMouseMove) - document.addEventListener("mouseup", onMouseUp) - }} +
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 81f4dc1cb..f9e717674 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -9,6 +9,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { ProgressCircle } from "@opencode-ai/ui/progress-circle" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Tabs } from "@opencode-ai/ui/tabs" import { Code } from "@opencode-ai/ui/code" import { SessionTurn } from "@opencode-ai/ui/session-turn" @@ -607,41 +608,14 @@ export default function Page() { class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base" style={{ height: `${layout.terminal.height()}px` }} > -
{ - e.preventDefault() - const startY = e.clientY - const startHeight = layout.terminal.height() - const maxHeight = window.innerHeight * 0.6 - const minHeight = 100 - const collapseThreshold = 50 - let currentHeight = startHeight - - document.body.style.userSelect = "none" - document.body.style.overflow = "hidden" - - const onMouseMove = (moveEvent: MouseEvent) => { - const deltaY = startY - moveEvent.clientY - currentHeight = startHeight + deltaY - const clampedHeight = Math.min(maxHeight, Math.max(minHeight, currentHeight)) - layout.terminal.resize(clampedHeight) - } - - const onMouseUp = () => { - document.body.style.userSelect = "" - document.body.style.overflow = "" - document.removeEventListener("mousemove", onMouseMove) - document.removeEventListener("mouseup", onMouseUp) - - if (currentHeight < collapseThreshold) { - layout.terminal.close() - } - } - - document.addEventListener("mousemove", onMouseMove) - document.addEventListener("mouseup", onMouseUp) - }} + diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css new file mode 100644 index 000000000..9344402c6 --- /dev/null +++ b/packages/ui/src/components/resize-handle.css @@ -0,0 +1,20 @@ +[data-component="resize-handle"] { + position: absolute; + z-index: 10; + + &[data-direction="horizontal"] { + inset-block: 0; + inset-inline-end: 0; + width: 8px; + transform: translateX(50%); + cursor: ew-resize; + } + + &[data-direction="vertical"] { + inset-inline: 0; + inset-block-start: 0; + height: 8px; + transform: translateY(-50%); + cursor: ns-resize; + } +} diff --git a/packages/ui/src/components/resize-handle.tsx b/packages/ui/src/components/resize-handle.tsx new file mode 100644 index 000000000..3ad01e27f --- /dev/null +++ b/packages/ui/src/components/resize-handle.tsx @@ -0,0 +1,71 @@ +import { splitProps, type JSX } from "solid-js" + +export interface ResizeHandleProps extends Omit, "onResize"> { + direction: "horizontal" | "vertical" + size: number + min: number + max: number + onResize: (size: number) => void + onCollapse?: () => void + collapseThreshold?: number +} + +export function ResizeHandle(props: ResizeHandleProps) { + const [local, rest] = splitProps(props, [ + "direction", + "size", + "min", + "max", + "onResize", + "onCollapse", + "collapseThreshold", + "class", + "classList", + ]) + + const handleMouseDown = (e: MouseEvent) => { + e.preventDefault() + const start = local.direction === "horizontal" ? e.clientX : e.clientY + const startSize = local.size + let current = startSize + + document.body.style.userSelect = "none" + document.body.style.overflow = "hidden" + + const onMouseMove = (moveEvent: MouseEvent) => { + const pos = local.direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY + const delta = local.direction === "vertical" ? start - pos : pos - start + current = startSize + delta + const clamped = Math.min(local.max, Math.max(local.min, current)) + local.onResize(clamped) + } + + const onMouseUp = () => { + document.body.style.userSelect = "" + document.body.style.overflow = "" + document.removeEventListener("mousemove", onMouseMove) + document.removeEventListener("mouseup", onMouseUp) + + const threshold = local.collapseThreshold ?? 0 + if (local.onCollapse && threshold > 0 && current < threshold) { + local.onCollapse() + } + } + + document.addEventListener("mousemove", onMouseMove) + document.addEventListener("mouseup", onMouseUp) + } + + return ( +
+ ) +} diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index e29d8e33b..afe005f84 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -25,6 +25,7 @@ @import "../components/message-progress.css" layer(components); @import "../components/message-nav.css" layer(components); @import "../components/progress-circle.css" layer(components); +@import "../components/resize-handle.css" layer(components); @import "../components/select.css" layer(components); @import "../components/select-dialog.css" layer(components); @import "../components/spinner.css" layer(components);