mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 11:26:24 +02:00
feat: sidebar resizing with mouse drag support
This commit is contained in:
parent
20a13df7e7
commit
64e118befd
2 changed files with 114 additions and 2 deletions
101
surfsense_web/components/layout/hooks/useSidebarResize.ts
Normal file
101
surfsense_web/components/layout/hooks/useSidebarResize.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const SIDEBAR_WIDTH_COOKIE_NAME = "sidebar_width";
|
||||||
|
const SIDEBAR_WIDTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
||||||
|
|
||||||
|
export const SIDEBAR_MIN_WIDTH = 240;
|
||||||
|
export const SIDEBAR_MAX_WIDTH = 480;
|
||||||
|
|
||||||
|
interface UseSidebarResizeReturn {
|
||||||
|
sidebarWidth: number;
|
||||||
|
handleMouseDown: (e: React.MouseEvent) => void;
|
||||||
|
isDragging: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebarResize(defaultWidth = SIDEBAR_MIN_WIDTH): UseSidebarResizeReturn {
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useState(defaultWidth);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const startWidthRef = useRef(defaultWidth);
|
||||||
|
|
||||||
|
// Initialize from cookie on mount
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const match = document.cookie.match(/(?:^|; )sidebar_width=([^;]+)/);
|
||||||
|
if (match) {
|
||||||
|
const parsed = Number(match[1]);
|
||||||
|
if (!Number.isNaN(parsed) && parsed >= SIDEBAR_MIN_WIDTH && parsed <= SIDEBAR_MAX_WIDTH) {
|
||||||
|
setSidebarWidth(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore cookie read errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Persist width to cookie
|
||||||
|
const persistWidth = useCallback((width: number) => {
|
||||||
|
try {
|
||||||
|
document.cookie = `${SIDEBAR_WIDTH_COOKIE_NAME}=${width}; path=/; max-age=${SIDEBAR_WIDTH_COOKIE_MAX_AGE}`;
|
||||||
|
} catch {
|
||||||
|
// Ignore cookie write errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
startXRef.current = e.clientX;
|
||||||
|
startWidthRef.current = sidebarWidth;
|
||||||
|
setIsDragging(true);
|
||||||
|
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
},
|
||||||
|
[sidebarWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const delta = e.clientX - startXRef.current;
|
||||||
|
const newWidth = Math.min(
|
||||||
|
SIDEBAR_MAX_WIDTH,
|
||||||
|
Math.max(SIDEBAR_MIN_WIDTH, startWidthRef.current + delta)
|
||||||
|
);
|
||||||
|
setSidebarWidth(newWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
|
||||||
|
// Persist the final width
|
||||||
|
setSidebarWidth((currentWidth) => {
|
||||||
|
persistWidth(currentWidth);
|
||||||
|
return currentWidth;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
};
|
||||||
|
}, [isDragging, persistWidth]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sidebarWidth,
|
||||||
|
handleMouseDown,
|
||||||
|
isDragging,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||||
|
import { useSidebarResize } from "../../hooks/useSidebarResize";
|
||||||
import { ChatListItem } from "./ChatListItem";
|
import { ChatListItem } from "./ChatListItem";
|
||||||
import { NavSection } from "./NavSection";
|
import { NavSection } from "./NavSection";
|
||||||
import { PageUsageDisplay } from "./PageUsageDisplay";
|
import { PageUsageDisplay } from "./PageUsageDisplay";
|
||||||
|
|
@ -82,15 +83,25 @@ export function Sidebar({
|
||||||
disableTooltips = false,
|
disableTooltips = false,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
|
const { sidebarWidth, handleMouseDown, isDragging } = useSidebarResize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full flex-col bg-sidebar text-sidebar-foreground transition-all duration-200 overflow-hidden",
|
"relative flex h-full flex-col bg-sidebar text-sidebar-foreground overflow-hidden",
|
||||||
isCollapsed ? "w-[60px]" : "w-[240px]",
|
isCollapsed ? "w-[60px] transition-all duration-200" : "",
|
||||||
|
!isCollapsed && !isDragging ? "transition-all duration-200" : "",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
style={!isCollapsed ? { width: sidebarWidth } : undefined}
|
||||||
>
|
>
|
||||||
|
{/* Resize handle on right border */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Header - search space name or collapse button when collapsed */}
|
{/* Header - search space name or collapse button when collapsed */}
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<div className="flex h-14 shrink-0 items-center justify-center border-b">
|
<div className="flex h-14 shrink-0 items-center justify-center border-b">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue