diff --git a/app-prefixable/src/components/resize-handle.tsx b/app-prefixable/src/components/resize-handle.tsx index 2c300a7..6c07d4c 100644 --- a/app-prefixable/src/components/resize-handle.tsx +++ b/app-prefixable/src/components/resize-handle.tsx @@ -8,15 +8,21 @@ export interface ResizeHandleProps { max: number onResize: (size: number) => void onCollapse?: () => void + onDragEnd?: () => void collapseThreshold?: number class?: string } export function ResizeHandle(props: ResizeHandleProps) { let cleanup: (() => void) | null = null + let dragging = false onCleanup(() => { if (cleanup) cleanup() + if (dragging) { + dragging = false + props.onDragEnd?.() + } }) const handleMouseDown = (e: MouseEvent) => { @@ -26,6 +32,7 @@ export function ResizeHandle(props: ResizeHandleProps) { const startSize = props.size let current = startSize + dragging = true document.body.style.userSelect = "none" document.body.style.overflow = "hidden" @@ -45,6 +52,7 @@ export function ResizeHandle(props: ResizeHandleProps) { } const onMouseUp = () => { + dragging = false document.body.style.userSelect = "" document.body.style.overflow = "" document.removeEventListener("mousemove", onMouseMove) @@ -55,6 +63,7 @@ export function ResizeHandle(props: ResizeHandleProps) { if (props.onCollapse && threshold > 0 && current < threshold) { props.onCollapse() } + props.onDragEnd?.() } cleanup = () => { diff --git a/app-prefixable/src/context/layout.tsx b/app-prefixable/src/context/layout.tsx index b8749d1..c1497f8 100644 --- a/app-prefixable/src/context/layout.tsx +++ b/app-prefixable/src/context/layout.tsx @@ -11,6 +11,9 @@ const LAYOUT_STORAGE_KEY = "opencode.layout"; // Default values const DEFAULT_REVIEW_WIDTH = 320; const DEFAULT_INFO_WIDTH = 256; +const DEFAULT_SIDEBAR_WIDTH = 256; +export const SIDEBAR_MIN_WIDTH = 180; +export const SIDEBAR_MAX_WIDTH = 480; interface PanelState { opened: boolean; @@ -25,6 +28,7 @@ export type FileTab = { interface LayoutState { review: PanelState; info: PanelState; + sidebar: { width?: number }; tabs?: FileTab[]; activeTab?: string | null; // null = Review tab, string = file path } @@ -48,6 +52,11 @@ interface LayoutContextValue { close: () => void; resize: (width: number) => void; }; + // Sidebar panel (sessions list) + sidebar: { + width: () => number; + resize: (width: number) => void; + }; // File tabs tabs: { all: () => FileTab[]; @@ -86,6 +95,9 @@ function loadState(): LayoutState { opened: parsed.info?.opened ?? false, width: parsed.info?.width ?? DEFAULT_INFO_WIDTH, }, + sidebar: { + width: parsed.sidebar?.width ?? DEFAULT_SIDEBAR_WIDTH, + }, tabs, activeTab, }; @@ -96,6 +108,7 @@ function loadState(): LayoutState { return { review: { opened: false, width: DEFAULT_REVIEW_WIDTH }, info: { opened: false, width: DEFAULT_INFO_WIDTH }, + sidebar: { width: DEFAULT_SIDEBAR_WIDTH }, tabs: [], activeTab: null, }; @@ -124,6 +137,13 @@ export function LayoutProvider(props: ParentProps) { initial.info.width ?? DEFAULT_INFO_WIDTH, ); + // Sidebar state (clamp loaded value to valid range) + const [sidebarWidth, setSidebarWidth] = createSignal( + Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, + initial.sidebar.width ?? DEFAULT_SIDEBAR_WIDTH, + )), + ); + // File tabs state const [fileTabs, setFileTabs] = createSignal(initial.tabs ?? []); const [activeTab, setActiveTab] = createSignal( @@ -135,6 +155,7 @@ export function LayoutProvider(props: ParentProps) { saveState({ review: { opened: reviewOpened(), width: reviewWidth() }, info: { opened: infoOpened(), width: infoWidth() }, + sidebar: { width: sidebarWidth() }, tabs: fileTabs(), activeTab: activeTab(), }); @@ -181,6 +202,13 @@ export function LayoutProvider(props: ParentProps) { persist(); }, }, + sidebar: { + width: sidebarWidth, + resize: (width: number) => { + setSidebarWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, width))); + persist(); + }, + }, tabs: { all: fileTabs, active: activeTab, diff --git a/app-prefixable/src/pages/layout.tsx b/app-prefixable/src/pages/layout.tsx index df680d9..a2c56dd 100644 --- a/app-prefixable/src/pages/layout.tsx +++ b/app-prefixable/src/pages/layout.tsx @@ -14,7 +14,7 @@ import { useSDK } from "../context/sdk"; import { useEvents } from "../context/events"; import { useProviders } from "../context/providers"; import { useTerminal } from "../context/terminal"; -import { useLayout } from "../context/layout"; +import { useLayout, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH } from "../context/layout"; import { base64Encode } from "../utils/path"; import { Spinner } from "../components/ui/spinner"; import { Button } from "../components/ui/button"; @@ -44,6 +44,7 @@ import { Pencil, } from "lucide-solid"; import { useSync } from "../context/sync"; +import { ResizeHandle } from "../components/resize-handle"; // Storage keys const PROJECTS_STORAGE_KEY = "opencode.projects"; @@ -107,6 +108,7 @@ export function Layout(props: ParentProps) { const [windowWidth, setWindowWidth] = createSignal( typeof window !== "undefined" ? window.innerWidth : 1200, ); + const [sidebarDragging, setSidebarDragging] = createSignal(false); // Responsive breakpoint - collapse sidebar below 900px const COLLAPSE_BREAKPOINT = 900; @@ -118,6 +120,11 @@ export function Layout(props: ParentProps) { return sidebarExpanded(); }); + // Reset dragging state when sidebar hides (unmount mid-drag safety) + createEffect(() => { + if (!showSidebar()) setSidebarDragging(false); + }); + // Load state from storage onMount(() => { // Load projects @@ -560,9 +567,9 @@ export function Layout(props: ParentProps) { {/* Sessions Panel (collapsible) */}
-
+
{/* Project Header with collapse toggle */}
- + {session.title || "Untitled"} @@ -764,50 +771,67 @@ export function Layout(props: ParentProps) {
-
@@ -846,10 +870,10 @@ export function Layout(props: ParentProps) {
{(session) => ( -
+
- + {session.title || "Untitled"} - +
+
+ +
+
)} @@ -941,6 +984,26 @@ export function Layout(props: ParentProps) {
+ {/* Sidebar resize handle */} + + { + setSidebarDragging(true); + layout.sidebar.resize(width); + }} + onDragEnd={() => { + setSidebarDragging(false); + }} + onCollapse={toggleSidebar} + collapseThreshold={100} + /> + + {/* Expand button when manually collapsed (not on settings or small screens) */}