summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-08 10:24:23 -0600
committerAdam <[email protected]>2025-12-08 10:24:26 -0600
commit5e3a59d5a297a8fc5589a9935ddf08db5c7e512c (patch)
treecbf7f8330c53f06c006a5b63dee11e75a89ec534
parent9f23d85e20af5ae8e81bdff66a0b5927f0597edc (diff)
downloadopencode-5e3a59d5a297a8fc5589a9935ddf08db5c7e512c.tar.gz
opencode-5e3a59d5a297a8fc5589a9935ddf08db5c7e512c.zip
feat: resize handle
-rw-r--r--packages/desktop/src/pages/layout.tsx44
-rw-r--r--packages/desktop/src/pages/session.tsx44
-rw-r--r--packages/ui/src/components/resize-handle.css20
-rw-r--r--packages/ui/src/components/resize-handle.tsx71
-rw-r--r--packages/ui/src/styles/index.css1
5 files changed, 110 insertions, 70 deletions
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 }}
>
<Show when={layout.sidebar.opened()}>
- <div
- class="absolute inset-y-0 right-0 z-10 w-2 translate-x-1/2 cursor-ew-resize"
- onMouseDown={(e) => {
- 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)
- }}
+ <ResizeHandle
+ direction="horizontal"
+ size={layout.sidebar.width()}
+ min={150}
+ max={window.innerWidth * 0.3}
+ collapseThreshold={80}
+ onResize={layout.sidebar.resize}
+ onCollapse={layout.sidebar.close}
/>
</Show>
<div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
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` }}
>
- <div
- class="absolute inset-x-0 top-0 z-10 h-2 -translate-y-1/2 cursor-ns-resize"
- onMouseDown={(e) => {
- 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)
- }}
+ <ResizeHandle
+ direction="vertical"
+ size={layout.terminal.height()}
+ min={100}
+ max={window.innerHeight * 0.6}
+ collapseThreshold={50}
+ onResize={layout.terminal.resize}
+ onCollapse={layout.terminal.close}
/>
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
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<JSX.HTMLAttributes<HTMLDivElement>, "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 (
+ <div
+ {...rest}
+ data-component="resize-handle"
+ data-direction={local.direction}
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ onMouseDown={handleMouseDown}
+ />
+ )
+}
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);