feat: draggable panel resize handles and remove extensions divider
Add ResizeHandle component for drag-to-resize side panels. ChatListPanel (180–400px) and RightPanel (200–480px) widths are persisted via localStorage. Also remove the border line below the Extensions page tab selector. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b64948cb4
commit
da07f1bd1b
5 files changed
+112
-7
No files matched your search
@@ -31,7 +31,7 @@ function ExtensionsPageInner() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b border-border/50 px-6 pt-4 pb-0">
|
||||
<div className="px-6 pt-4 pb-0">
|
||||
<h1 className="text-xl font-semibold mb-3">Extensions</h1>
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as ExtTab)}>
|
||||
<TabsList>
|
||||
|
||||
@@ -6,10 +6,16 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { NavRail } from "./NavRail";
|
||||
import { ChatListPanel } from "./ChatListPanel";
|
||||
import { RightPanel } from "./RightPanel";
|
||||
import { ResizeHandle } from "./ResizeHandle";
|
||||
import { UpdateDialog } from "./UpdateDialog";
|
||||
import { PanelContext, type PanelContent } from "@/hooks/usePanel";
|
||||
import { UpdateContext, type UpdateInfo } from "@/hooks/useUpdate";
|
||||
|
||||
const CHATLIST_MIN = 180;
|
||||
const CHATLIST_MAX = 400;
|
||||
const RIGHTPANEL_MIN = 200;
|
||||
const RIGHTPANEL_MAX = 480;
|
||||
|
||||
const LG_BREAKPOINT = 1024;
|
||||
const CHECK_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours
|
||||
const DISMISSED_VERSION_KEY = "codepilot_dismissed_update_version";
|
||||
@@ -19,6 +25,36 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const [chatListOpen, setChatListOpenRaw] = useState(false);
|
||||
|
||||
// Panel width state with localStorage persistence
|
||||
const [chatListWidth, setChatListWidth] = useState(() => {
|
||||
if (typeof window === "undefined") return 240;
|
||||
return parseInt(localStorage.getItem("codepilot_chatlist_width") || "240");
|
||||
});
|
||||
const [rightPanelWidth, setRightPanelWidth] = useState(() => {
|
||||
if (typeof window === "undefined") return 288;
|
||||
return parseInt(localStorage.getItem("codepilot_rightpanel_width") || "288");
|
||||
});
|
||||
|
||||
const handleChatListResize = useCallback((delta: number) => {
|
||||
setChatListWidth((w) => Math.min(CHATLIST_MAX, Math.max(CHATLIST_MIN, w + delta)));
|
||||
}, []);
|
||||
const handleChatListResizeEnd = useCallback(() => {
|
||||
setChatListWidth((w) => {
|
||||
localStorage.setItem("codepilot_chatlist_width", String(w));
|
||||
return w;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRightPanelResize = useCallback((delta: number) => {
|
||||
setRightPanelWidth((w) => Math.min(RIGHTPANEL_MAX, Math.max(RIGHTPANEL_MIN, w - delta)));
|
||||
}, []);
|
||||
const handleRightPanelResizeEnd = useCallback(() => {
|
||||
setRightPanelWidth((w) => {
|
||||
localStorage.setItem("codepilot_rightpanel_width", String(w));
|
||||
return w;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Panel state
|
||||
const isChatRoute = pathname.startsWith("/chat/") || pathname === "/chat";
|
||||
const isChatDetailRoute = pathname.startsWith("/chat/");
|
||||
@@ -165,7 +201,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
hasUpdate={updateInfo?.updateAvailable ?? false}
|
||||
skipPermissionsActive={skipPermissionsActive}
|
||||
/>
|
||||
<ChatListPanel open={chatListOpen} />
|
||||
<ChatListPanel open={chatListOpen} width={chatListWidth} />
|
||||
{chatListOpen && (
|
||||
<ResizeHandle side="left" onResize={handleChatListResize} onResizeEnd={handleChatListResizeEnd} />
|
||||
)}
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{/* Electron draggable title bar region */}
|
||||
<div
|
||||
@@ -174,7 +213,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
/>
|
||||
<main className="relative flex-1 overflow-hidden">{children}</main>
|
||||
</div>
|
||||
{isChatDetailRoute && <RightPanel />}
|
||||
{isChatDetailRoute && panelOpen && (
|
||||
<ResizeHandle side="right" onResize={handleRightPanelResize} onResizeEnd={handleRightPanelResizeEnd} />
|
||||
)}
|
||||
{isChatDetailRoute && <RightPanel width={rightPanelWidth} />}
|
||||
</div>
|
||||
<UpdateDialog />
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { ChatSession } from "@/types";
|
||||
|
||||
interface ChatListPanelProps {
|
||||
open: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateStr: string): string {
|
||||
@@ -69,7 +70,7 @@ const MODE_BADGE_CONFIG = {
|
||||
ask: { label: "Ask", className: "bg-green-500/10 text-green-500" },
|
||||
} as const;
|
||||
|
||||
export function ChatListPanel({ open }: ChatListPanelProps) {
|
||||
export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { streamingSessionId, pendingApprovalSessionId } = usePanel();
|
||||
@@ -150,7 +151,7 @@ export function ChatListPanel({ open }: ChatListPanelProps) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<aside className="hidden h-full w-60 shrink-0 flex-col overflow-hidden bg-sidebar lg:flex">
|
||||
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-sidebar lg:flex" style={{ width: width ?? 240 }}>
|
||||
{/* Header - extra top padding for macOS traffic lights */}
|
||||
<div className="flex h-12 shrink-0 items-center justify-between px-3 mt-5 pl-6">
|
||||
<span className="text-[13px] font-semibold tracking-tight text-sidebar-foreground">
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ResizeHandleProps {
|
||||
side: "left" | "right";
|
||||
onResize: (delta: number) => void;
|
||||
onResizeEnd?: () => void;
|
||||
}
|
||||
|
||||
export function ResizeHandle({ side, onResize, onResizeEnd }: ResizeHandleProps) {
|
||||
const isDragging = useRef(false);
|
||||
const startX = useRef(0);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
isDragging.current = true;
|
||||
startX.current = e.clientX;
|
||||
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
const handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!isDragging.current) return;
|
||||
const delta = ev.clientX - startX.current;
|
||||
startX.current = ev.clientX;
|
||||
onResize(delta);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDragging.current = false;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
onResizeEnd?.();
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
},
|
||||
[onResize, onResizeEnd]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className={cn(
|
||||
"group relative z-10 flex w-1 shrink-0 cursor-col-resize items-center justify-center",
|
||||
side === "left" ? "-ml-0.5" : "-mr-0.5"
|
||||
)}
|
||||
>
|
||||
<div className="h-full w-px bg-transparent transition-colors duration-150 group-hover:bg-border" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,11 @@ import { usePanel } from "@/hooks/usePanel";
|
||||
import { FileTree } from "@/components/project/FileTree";
|
||||
import { FilePreview } from "@/components/project/FilePreview";
|
||||
|
||||
export function RightPanel() {
|
||||
interface RightPanelProps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function RightPanel({ width }: RightPanelProps) {
|
||||
const { panelOpen, setPanelOpen, workingDirectory, sessionId, sessionTitle, setSessionTitle } = usePanel();
|
||||
const [previewPath, setPreviewPath] = useState<string | null>(null);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
@@ -93,7 +97,7 @@ export function RightPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="hidden h-full w-72 shrink-0 flex-col overflow-hidden bg-background lg:flex">
|
||||
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-background lg:flex" style={{ width: width ?? 288 }}>
|
||||
{/* Header */}
|
||||
<div className="flex h-10 shrink-0 items-center justify-between px-4">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Chat Info</span>
|
||||
|
||||
Reference in new issue
Block a user