mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-20 23:21:06 +02:00
refactor: consolidated secondary sidebars into a reusable slide-out panel
This commit is contained in:
parent
ed5f0d10e8
commit
4005e03ec4
6 changed files with 158 additions and 178 deletions
|
|
@ -33,9 +33,6 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||||
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
||||||
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
||||||
import { LayoutShell } from "../ui/shell";
|
import { LayoutShell } from "../ui/shell";
|
||||||
import { AllPrivateChatsSidebar } from "../ui/sidebar/AllPrivateChatsSidebar";
|
|
||||||
import { AllSharedChatsSidebar } from "../ui/sidebar/AllSharedChatsSidebar";
|
|
||||||
|
|
||||||
interface LayoutDataProviderProps {
|
interface LayoutDataProviderProps {
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -390,7 +387,13 @@ export function LayoutDataProvider({
|
||||||
(item: NavItem) => {
|
(item: NavItem) => {
|
||||||
// Handle inbox specially - toggle sidebar instead of navigating
|
// Handle inbox specially - toggle sidebar instead of navigating
|
||||||
if (item.url === "#inbox") {
|
if (item.url === "#inbox") {
|
||||||
setIsInboxSidebarOpen((prev) => !prev);
|
setIsInboxSidebarOpen((prev) => {
|
||||||
|
if (!prev) {
|
||||||
|
setIsAllSharedChatsSidebarOpen(false);
|
||||||
|
setIsAllPrivateChatsSidebarOpen(false);
|
||||||
|
}
|
||||||
|
return !prev;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(item.url);
|
router.push(item.url);
|
||||||
|
|
@ -490,10 +493,14 @@ export function LayoutDataProvider({
|
||||||
|
|
||||||
const handleViewAllSharedChats = useCallback(() => {
|
const handleViewAllSharedChats = useCallback(() => {
|
||||||
setIsAllSharedChatsSidebarOpen(true);
|
setIsAllSharedChatsSidebarOpen(true);
|
||||||
|
setIsAllPrivateChatsSidebarOpen(false);
|
||||||
|
setIsInboxSidebarOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleViewAllPrivateChats = useCallback(() => {
|
const handleViewAllPrivateChats = useCallback(() => {
|
||||||
setIsAllPrivateChatsSidebarOpen(true);
|
setIsAllPrivateChatsSidebarOpen(true);
|
||||||
|
setIsAllSharedChatsSidebarOpen(false);
|
||||||
|
setIsInboxSidebarOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Delete handlers
|
// Delete handlers
|
||||||
|
|
@ -614,6 +621,16 @@ export function LayoutDataProvider({
|
||||||
isDocked: isInboxDocked,
|
isDocked: isInboxDocked,
|
||||||
onDockedChange: setIsInboxDocked,
|
onDockedChange: setIsInboxDocked,
|
||||||
}}
|
}}
|
||||||
|
allSharedChatsPanel={{
|
||||||
|
open: isAllSharedChatsSidebarOpen,
|
||||||
|
onOpenChange: setIsAllSharedChatsSidebarOpen,
|
||||||
|
searchSpaceId,
|
||||||
|
}}
|
||||||
|
allPrivateChatsPanel={{
|
||||||
|
open: isAllPrivateChatsSidebarOpen,
|
||||||
|
onOpenChange: setIsAllPrivateChatsSidebarOpen,
|
||||||
|
searchSpaceId,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</LayoutShell>
|
</LayoutShell>
|
||||||
|
|
@ -796,20 +813,6 @@ export function LayoutDataProvider({
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* All Shared Chats Sidebar */}
|
|
||||||
<AllSharedChatsSidebar
|
|
||||||
open={isAllSharedChatsSidebarOpen}
|
|
||||||
onOpenChange={setIsAllSharedChatsSidebarOpen}
|
|
||||||
searchSpaceId={searchSpaceId}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* All Private Chats Sidebar */}
|
|
||||||
<AllPrivateChatsSidebar
|
|
||||||
open={isAllPrivateChatsSidebarOpen}
|
|
||||||
onOpenChange={setIsAllPrivateChatsSidebarOpen}
|
|
||||||
searchSpaceId={searchSpaceId}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Create Search Space Dialog */}
|
{/* Create Search Space Dialog */}
|
||||||
<CreateSearchSpaceDialog
|
<CreateSearchSpaceDialog
|
||||||
open={isCreateSearchSpaceDialogOpen}
|
open={isCreateSearchSpaceDialogOpen}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,14 @@ import { useSidebarResize } from "../../hooks/useSidebarResize";
|
||||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||||
import { Header } from "../header";
|
import { Header } from "../header";
|
||||||
import { IconRail } from "../icon-rail";
|
import { IconRail } from "../icon-rail";
|
||||||
import { InboxSidebar, MobileSidebar, MobileSidebarTrigger, Sidebar } from "../sidebar";
|
import {
|
||||||
|
AllPrivateChatsSidebar,
|
||||||
|
AllSharedChatsSidebar,
|
||||||
|
InboxSidebar,
|
||||||
|
MobileSidebar,
|
||||||
|
MobileSidebarTrigger,
|
||||||
|
Sidebar,
|
||||||
|
} from "../sidebar";
|
||||||
|
|
||||||
// Tab-specific data source props
|
// Tab-specific data source props
|
||||||
interface TabDataSource {
|
interface TabDataSource {
|
||||||
|
|
@ -76,6 +83,9 @@ interface LayoutShellProps {
|
||||||
// Inbox props
|
// Inbox props
|
||||||
inbox?: InboxProps;
|
inbox?: InboxProps;
|
||||||
isLoadingChats?: boolean;
|
isLoadingChats?: boolean;
|
||||||
|
// All chats panel props
|
||||||
|
allSharedChatsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string };
|
||||||
|
allPrivateChatsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutShell({
|
export function LayoutShell({
|
||||||
|
|
@ -113,6 +123,8 @@ export function LayoutShell({
|
||||||
className,
|
className,
|
||||||
inbox,
|
inbox,
|
||||||
isLoadingChats = false,
|
isLoadingChats = false,
|
||||||
|
allSharedChatsPanel,
|
||||||
|
allPrivateChatsPanel,
|
||||||
}: LayoutShellProps) {
|
}: LayoutShellProps) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
@ -280,6 +292,24 @@ export function LayoutShell({
|
||||||
onDockedChange={inbox.onDockedChange}
|
onDockedChange={inbox.onDockedChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* All Shared Chats - slide-out panel */}
|
||||||
|
{allSharedChatsPanel && (
|
||||||
|
<AllSharedChatsSidebar
|
||||||
|
open={allSharedChatsPanel.open}
|
||||||
|
onOpenChange={allSharedChatsPanel.onOpenChange}
|
||||||
|
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* All Private Chats - slide-out panel */}
|
||||||
|
{allPrivateChatsPanel && (
|
||||||
|
<AllPrivateChatsSidebar
|
||||||
|
open={allPrivateChatsPanel.open}
|
||||||
|
onOpenChange={allPrivateChatsPanel.onOpenChange}
|
||||||
|
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ import {
|
||||||
User,
|
User,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,6 +38,7 @@ import {
|
||||||
updateThread,
|
updateThread,
|
||||||
} from "@/lib/chat/thread-persistence";
|
} from "@/lib/chat/thread-persistence";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||||
|
|
||||||
interface AllPrivateChatsSidebarProps {
|
interface AllPrivateChatsSidebarProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -69,16 +68,11 @@ export function AllPrivateChatsSidebar({
|
||||||
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
|
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape" && open) {
|
if (e.key === "Escape" && open) {
|
||||||
|
|
@ -89,17 +83,6 @@ export function AllPrivateChatsSidebar({
|
||||||
return () => document.removeEventListener("keydown", handleEscape);
|
return () => document.removeEventListener("keydown", handleEscape);
|
||||||
}, [open, onOpenChange]);
|
}, [open, onOpenChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
};
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: threadsData,
|
data: threadsData,
|
||||||
error: threadsError,
|
error: threadsError,
|
||||||
|
|
@ -214,33 +197,13 @@ export function AllPrivateChatsSidebar({
|
||||||
const activeCount = activeChats.length;
|
const activeCount = activeChats.length;
|
||||||
const archivedCount = archivedChats.length;
|
const archivedCount = archivedChats.length;
|
||||||
|
|
||||||
if (!mounted) return null;
|
return (
|
||||||
|
<SidebarSlideOutPanel
|
||||||
return createPortal(
|
open={open}
|
||||||
<AnimatePresence>
|
onOpenChange={onOpenChange}
|
||||||
{open && (
|
ariaLabel={t("chats") || "Private Chats"}
|
||||||
<>
|
>
|
||||||
<motion.div
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="fixed inset-0 z-70 bg-black/50"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ x: "-100%" }}
|
|
||||||
animate={{ x: 0 }}
|
|
||||||
exit={{ x: "-100%" }}
|
|
||||||
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
|
|
||||||
className="fixed inset-y-0 left-0 z-70 w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label={t("chats") || "Private Chats"}
|
|
||||||
>
|
|
||||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="h-5 w-5 text-primary" />
|
<User className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
||||||
|
|
@ -451,11 +414,7 @@ export function AllPrivateChatsSidebar({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</SidebarSlideOutPanel>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>,
|
|
||||||
document.body
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ import {
|
||||||
Users,
|
Users,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,6 +38,7 @@ import {
|
||||||
updateThread,
|
updateThread,
|
||||||
} from "@/lib/chat/thread-persistence";
|
} from "@/lib/chat/thread-persistence";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||||
|
|
||||||
interface AllSharedChatsSidebarProps {
|
interface AllSharedChatsSidebarProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -69,16 +68,11 @@ export function AllSharedChatsSidebar({
|
||||||
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
|
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape" && open) {
|
if (e.key === "Escape" && open) {
|
||||||
|
|
@ -89,17 +83,6 @@ export function AllSharedChatsSidebar({
|
||||||
return () => document.removeEventListener("keydown", handleEscape);
|
return () => document.removeEventListener("keydown", handleEscape);
|
||||||
}, [open, onOpenChange]);
|
}, [open, onOpenChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
};
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: threadsData,
|
data: threadsData,
|
||||||
error: threadsError,
|
error: threadsError,
|
||||||
|
|
@ -214,33 +197,13 @@ export function AllSharedChatsSidebar({
|
||||||
const activeCount = activeChats.length;
|
const activeCount = activeChats.length;
|
||||||
const archivedCount = archivedChats.length;
|
const archivedCount = archivedChats.length;
|
||||||
|
|
||||||
if (!mounted) return null;
|
return (
|
||||||
|
<SidebarSlideOutPanel
|
||||||
return createPortal(
|
open={open}
|
||||||
<AnimatePresence>
|
onOpenChange={onOpenChange}
|
||||||
{open && (
|
ariaLabel={t("shared_chats") || "Shared Chats"}
|
||||||
<>
|
>
|
||||||
<motion.div
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="fixed inset-0 z-70 bg-black/50"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ x: "-100%" }}
|
|
||||||
animate={{ x: 0 }}
|
|
||||||
exit={{ x: "-100%" }}
|
|
||||||
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
|
|
||||||
className="fixed inset-y-0 left-0 z-70 w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label={t("shared_chats") || "Shared Chats"}
|
|
||||||
>
|
|
||||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="h-5 w-5 text-primary" />
|
<Users className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
||||||
|
|
@ -451,11 +414,7 @@ export function AllSharedChatsSidebar({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</SidebarSlideOutPanel>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>,
|
|
||||||
document.body
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import {
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
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 { notificationsApiService } from "@/lib/apis/notifications-api.service";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useSidebarContextSafe } from "../../hooks";
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||||
|
|
||||||
// Sidebar width constant for collapsed state
|
|
||||||
const SIDEBAR_COLLAPSED_WIDTH = 60;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get initials from name or email for avatar fallback
|
* 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;
|
if (!mounted) return null;
|
||||||
|
|
||||||
// Shared content component for both docked and floating modes
|
// 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
|
// FLOATING MODE: Render with animation and click-away layer
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel={t("inbox") || "Inbox"}>
|
||||||
{open && (
|
{inboxContent}
|
||||||
<>
|
</SidebarSlideOutPanel>
|
||||||
{/* Click-away layer - only covers the content area, not the sidebar */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
style={{
|
|
||||||
left: isMobile ? 0 : sidebarWidth,
|
|
||||||
}}
|
|
||||||
className="absolute inset-y-0 right-0"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Clip container - positioned at sidebar edge with overflow hidden */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
left: isMobile ? 0 : sidebarWidth,
|
|
||||||
width: isMobile ? "100%" : 360,
|
|
||||||
}}
|
|
||||||
className={cn("absolute z-10 overflow-hidden pointer-events-none", "inset-y-0")}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ x: "-100%" }}
|
|
||||||
animate={{ x: 0 }}
|
|
||||||
exit={{ x: "-100%" }}
|
|
||||||
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
|
||||||
className={cn(
|
|
||||||
"h-full w-full bg-background flex flex-col pointer-events-auto",
|
|
||||||
"sm:border-r sm:shadow-xl"
|
|
||||||
)}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label={t("inbox") || "Inbox"}
|
|
||||||
>
|
|
||||||
{inboxContent}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
{/* Click-away layer - covers the full container including the sidebar */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute inset-0 z-[5]"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Clip container - positioned at sidebar edge with overflow hidden */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
left: isMobile ? 0 : sidebarWidth,
|
||||||
|
width: isMobile ? "100%" : width,
|
||||||
|
}}
|
||||||
|
className={cn("absolute z-10 overflow-hidden pointer-events-none", "inset-y-0")}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "-100%" }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: "-100%" }}
|
||||||
|
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full bg-background flex flex-col pointer-events-auto",
|
||||||
|
"sm:border-r sm:shadow-xl"
|
||||||
|
)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue