Adam 2 месяцев назад
Родитель
Сommit
5e3a59d5a2

+ 9 - 35
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">

+ 9 - 35
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">

+ 20 - 0
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;
+  }
+}

+ 71 - 0
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}
+    />
+  )
+}

+ 1 - 0
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);