diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index ead017a3e..e40fe28ad 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -33,9 +33,6 @@ import { cacheKeys } from "@/lib/query-client/cache-keys"; import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types"; import { CreateSearchSpaceDialog } from "../ui/dialogs"; import { LayoutShell } from "../ui/shell"; -import { AllPrivateChatsSidebar } from "../ui/sidebar/AllPrivateChatsSidebar"; -import { AllSharedChatsSidebar } from "../ui/sidebar/AllSharedChatsSidebar"; - interface LayoutDataProviderProps { searchSpaceId: string; children: React.ReactNode; @@ -390,7 +387,13 @@ export function LayoutDataProvider({ (item: NavItem) => { // Handle inbox specially - toggle sidebar instead of navigating if (item.url === "#inbox") { - setIsInboxSidebarOpen((prev) => !prev); + setIsInboxSidebarOpen((prev) => { + if (!prev) { + setIsAllSharedChatsSidebarOpen(false); + setIsAllPrivateChatsSidebarOpen(false); + } + return !prev; + }); return; } router.push(item.url); @@ -490,10 +493,14 @@ export function LayoutDataProvider({ const handleViewAllSharedChats = useCallback(() => { setIsAllSharedChatsSidebarOpen(true); + setIsAllPrivateChatsSidebarOpen(false); + setIsInboxSidebarOpen(false); }, []); const handleViewAllPrivateChats = useCallback(() => { setIsAllPrivateChatsSidebarOpen(true); + setIsAllSharedChatsSidebarOpen(false); + setIsInboxSidebarOpen(false); }, []); // Delete handlers @@ -614,6 +621,16 @@ export function LayoutDataProvider({ isDocked: isInboxDocked, onDockedChange: setIsInboxDocked, }} + allSharedChatsPanel={{ + open: isAllSharedChatsSidebarOpen, + onOpenChange: setIsAllSharedChatsSidebarOpen, + searchSpaceId, + }} + allPrivateChatsPanel={{ + open: isAllPrivateChatsSidebarOpen, + onOpenChange: setIsAllPrivateChatsSidebarOpen, + searchSpaceId, + }} > {children} @@ -796,20 +813,6 @@ export function LayoutDataProvider({ - {/* All Shared Chats Sidebar */} - - - {/* All Private Chats Sidebar */} - - {/* Create Search Space Dialog */} void; searchSpaceId: string }; + allPrivateChatsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string }; } export function LayoutShell({ @@ -113,6 +123,8 @@ export function LayoutShell({ className, inbox, isLoadingChats = false, + allSharedChatsPanel, + allPrivateChatsPanel, }: LayoutShellProps) { const isMobile = useIsMobile(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); @@ -280,6 +292,24 @@ export function LayoutShell({ onDockedChange={inbox.onDockedChange} /> )} + + {/* All Shared Chats - slide-out panel */} + {allSharedChatsPanel && ( + + )} + + {/* All Private Chats - slide-out panel */} + {allPrivateChatsPanel && ( + + )} diff --git a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx index 1d4f590bd..c78adfef1 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx @@ -12,11 +12,9 @@ import { User, X, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -40,6 +38,7 @@ import { updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; +import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; interface AllPrivateChatsSidebarProps { open: boolean; @@ -69,16 +68,11 @@ export function AllPrivateChatsSidebar({ const [archivingThreadId, setArchivingThreadId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showArchived, setShowArchived] = useState(false); - const [mounted, setMounted] = useState(false); const [openDropdownId, setOpenDropdownId] = useState(null); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const isSearchMode = !!debouncedSearchQuery.trim(); - useEffect(() => { - setMounted(true); - }, []); - useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { @@ -89,17 +83,6 @@ export function AllPrivateChatsSidebar({ return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - const { data: threadsData, error: threadsError, @@ -214,33 +197,13 @@ export function AllPrivateChatsSidebar({ const activeCount = activeChats.length; const archivedCount = archivedChats.length; - if (!mounted) return null; - - return createPortal( - - {open && ( - <> - onOpenChange(false)} - aria-hidden="true" - /> - - -
+ return ( + +

{t("chats") || "Private Chats"}

@@ -451,11 +414,7 @@ export function AllPrivateChatsSidebar({ )}
)} -
- - - )} - , - document.body +
+ ); } diff --git a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx index db7ca73b2..ed22b819d 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx @@ -12,11 +12,9 @@ import { Users, X, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -40,6 +38,7 @@ import { updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; +import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; interface AllSharedChatsSidebarProps { open: boolean; @@ -69,16 +68,11 @@ export function AllSharedChatsSidebar({ const [archivingThreadId, setArchivingThreadId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showArchived, setShowArchived] = useState(false); - const [mounted, setMounted] = useState(false); const [openDropdownId, setOpenDropdownId] = useState(null); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const isSearchMode = !!debouncedSearchQuery.trim(); - useEffect(() => { - setMounted(true); - }, []); - useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { @@ -89,17 +83,6 @@ export function AllSharedChatsSidebar({ return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - const { data: threadsData, error: threadsError, @@ -214,33 +197,13 @@ export function AllSharedChatsSidebar({ const activeCount = activeChats.length; const archivedCount = archivedChats.length; - if (!mounted) return null; - - return createPortal( - - {open && ( - <> - onOpenChange(false)} - aria-hidden="true" - /> - - -
+ return ( + +

{t("shared_chats") || "Shared Chats"}

@@ -451,11 +414,7 @@ export function AllSharedChatsSidebar({ )}
)} -
- - - )} - , - document.body +
+ ); } diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index 5a709a8dd..c459141d6 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -19,7 +19,6 @@ import { Search, X, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -59,10 +58,7 @@ import { useMediaQuery } from "@/hooks/use-media-query"; import { notificationsApiService } from "@/lib/apis/notifications-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cn } from "@/lib/utils"; -import { useSidebarContextSafe } from "../../hooks"; - -// Sidebar width constant for collapsed state -const SIDEBAR_COLLAPSED_WIDTH = 60; +import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; /** * Get initials from name or email for avatar fallback @@ -560,14 +556,6 @@ export function InboxSidebar({ }; }; - // Get sidebar collapsed state from context (provided by LayoutShell) - const sidebarContext = useSidebarContextSafe(); - const isCollapsed = sidebarContext?.isCollapsed ?? false; - - // Calculate the left position for the inbox panel (relative to sidebar) - // Use dynamic width from context (tracks resize) for expanded state - const sidebarWidth = isCollapsed ? SIDEBAR_COLLAPSED_WIDTH : (sidebarContext?.sidebarWidth ?? 240); - if (!mounted) return null; // Shared content component for both docked and floating modes @@ -1126,49 +1114,8 @@ export function InboxSidebar({ // FLOATING MODE: Render with animation and click-away layer return ( - - {open && ( - <> - {/* Click-away layer - only covers the content area, not the sidebar */} - onOpenChange(false)} - aria-hidden="true" - /> - - {/* Clip container - positioned at sidebar edge with overflow hidden */} -
- - {inboxContent} - -
- - )} -
+ + {inboxContent} + ); } diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx new file mode 100644 index 000000000..9d74c01f1 --- /dev/null +++ b/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { AnimatePresence, motion } from "motion/react"; +import { cn } from "@/lib/utils"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { useSidebarContextSafe } from "../../hooks"; + +const SIDEBAR_COLLAPSED_WIDTH = 60; + +interface SidebarSlideOutPanelProps { + open: boolean; + onOpenChange: (open: boolean) => void; + ariaLabel: string; + width?: number; + children: React.ReactNode; +} + +/** + * Reusable slide-out panel that appears from the right edge of the sidebar. + * Used by InboxSidebar (floating mode), AllSharedChatsSidebar, and AllPrivateChatsSidebar. + * + * Must be rendered inside a positioned container (the LayoutShell's relative flex container) + * and within the SidebarProvider context. + */ +export function SidebarSlideOutPanel({ + open, + onOpenChange, + ariaLabel, + width = 360, + children, +}: SidebarSlideOutPanelProps) { + const isMobile = !useMediaQuery("(min-width: 640px)"); + const sidebarContext = useSidebarContextSafe(); + const isCollapsed = sidebarContext?.isCollapsed ?? false; + const sidebarWidth = isCollapsed + ? SIDEBAR_COLLAPSED_WIDTH + : (sidebarContext?.sidebarWidth ?? 240); + + return ( + + {open && ( + <> + {/* Click-away layer - covers the full container including the sidebar */} + onOpenChange(false)} + aria-hidden="true" + /> + + {/* Clip container - positioned at sidebar edge with overflow hidden */} +
+ + {children} + +
+ + )} +
+ ); +}