mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 11:26:24 +02:00
merge upstream/dev into feat/migrate-electric-to-zero
Resolve 8 conflicts: - Accept upstream deletion of 3 composio_*_connector.py (unified Google connectors) - Accept our deletion of ElectricProvider.tsx, use-connectors-electric.ts, use-messages-electric.ts (replaced by Zero equivalents) - Keep both new deps in package.json (@rocicorp/zero + @slate-serializers/html) - Regenerate pnpm-lock.yaml
This commit is contained in:
commit
5d8a62a4a6
207 changed files with 28023 additions and 12247 deletions
|
|
@ -127,12 +127,12 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
||||
// Separate sidebar states for shared and private chats
|
||||
const [isAllSharedChatsSidebarOpen, setIsAllSharedChatsSidebarOpen] = useState(false);
|
||||
const [isAllPrivateChatsSidebarOpen, setIsAllPrivateChatsSidebarOpen] = useState(false);
|
||||
// Unified slide-out panel state (only one can be open at a time)
|
||||
type SlideoutPanel = "inbox" | "shared" | "private" | "announcements" | null;
|
||||
const [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null);
|
||||
|
||||
// Inbox sidebar state
|
||||
const [isInboxSidebarOpen, setIsInboxSidebarOpen] = useState(false);
|
||||
const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
|
||||
const isAnnouncementsSidebarOpen = activeSlideoutPanel === "announcements";
|
||||
|
||||
// Documents sidebar state (shared atom so Composer can toggle it)
|
||||
const [isDocumentsSidebarOpen, setIsDocumentsSidebarOpen] = useAtom(documentsSidebarOpenAtom);
|
||||
|
|
@ -151,9 +151,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
}
|
||||
}, [setIsDocumentsSidebarOpen]);
|
||||
|
||||
// Announcements sidebar state
|
||||
const [isAnnouncementsSidebarOpen, setIsAnnouncementsSidebarOpen] = useState(false);
|
||||
|
||||
// Search space dialog state
|
||||
const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false);
|
||||
|
||||
|
|
@ -268,12 +265,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
}, [pendingNewChat, params?.chat_id, router, searchSpaceId, resetCurrentThread]);
|
||||
|
||||
// Reset transient slide-out panels when switching search spaces.
|
||||
// Some browsers can retain overlay/backdrop state across route transitions.
|
||||
useEffect(() => {
|
||||
setIsInboxSidebarOpen(false);
|
||||
setIsAllSharedChatsSidebarOpen(false);
|
||||
setIsAllPrivateChatsSidebarOpen(false);
|
||||
setIsAnnouncementsSidebarOpen(false);
|
||||
setActiveSlideoutPanel(null);
|
||||
}, [searchSpaceId]);
|
||||
|
||||
const searchSpaces: SearchSpace[] = useMemo(() => {
|
||||
|
|
@ -483,14 +476,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
const handleNavItemClick = useCallback(
|
||||
(item: NavItem) => {
|
||||
if (item.url === "#inbox") {
|
||||
setIsInboxSidebarOpen((prev) => {
|
||||
if (!prev) {
|
||||
setIsAllSharedChatsSidebarOpen(false);
|
||||
setIsAllPrivateChatsSidebarOpen(false);
|
||||
setIsAnnouncementsSidebarOpen(false);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
setActiveSlideoutPanel((prev) => (prev === "inbox" ? null : "inbox"));
|
||||
return;
|
||||
}
|
||||
if (item.url === "#documents") {
|
||||
|
|
@ -498,20 +484,14 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
if (!isDocumentsSidebarOpen) {
|
||||
setIsDocumentsSidebarOpen(true);
|
||||
setIsRightPanelCollapsed(false);
|
||||
setIsInboxSidebarOpen(false);
|
||||
setIsAllSharedChatsSidebarOpen(false);
|
||||
setIsAllPrivateChatsSidebarOpen(false);
|
||||
setIsAnnouncementsSidebarOpen(false);
|
||||
setActiveSlideoutPanel(null);
|
||||
} else {
|
||||
setIsRightPanelCollapsed((prev) => !prev);
|
||||
}
|
||||
} else {
|
||||
setIsDocumentsSidebarOpen((prev) => {
|
||||
if (!prev) {
|
||||
setIsInboxSidebarOpen(false);
|
||||
setIsAllSharedChatsSidebarOpen(false);
|
||||
setIsAllPrivateChatsSidebarOpen(false);
|
||||
setIsAnnouncementsSidebarOpen(false);
|
||||
setActiveSlideoutPanel(null);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
|
|
@ -519,14 +499,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
return;
|
||||
}
|
||||
if (item.url === "#announcements") {
|
||||
setIsAnnouncementsSidebarOpen((prev) => {
|
||||
if (!prev) {
|
||||
setIsInboxSidebarOpen(false);
|
||||
setIsAllSharedChatsSidebarOpen(false);
|
||||
setIsAllPrivateChatsSidebarOpen(false);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
setActiveSlideoutPanel((prev) => (prev === "announcements" ? null : "announcements"));
|
||||
return;
|
||||
}
|
||||
router.push(item.url);
|
||||
|
|
@ -617,25 +590,11 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
}, [router]);
|
||||
|
||||
const handleViewAllSharedChats = useCallback(() => {
|
||||
setIsAllSharedChatsSidebarOpen((prev) => {
|
||||
if (!prev) {
|
||||
setIsAllPrivateChatsSidebarOpen(false);
|
||||
setIsInboxSidebarOpen(false);
|
||||
setIsAnnouncementsSidebarOpen(false);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
setActiveSlideoutPanel((prev) => (prev === "shared" ? null : "shared"));
|
||||
}, []);
|
||||
|
||||
const handleViewAllPrivateChats = useCallback(() => {
|
||||
setIsAllPrivateChatsSidebarOpen((prev) => {
|
||||
if (!prev) {
|
||||
setIsAllSharedChatsSidebarOpen(false);
|
||||
setIsInboxSidebarOpen(false);
|
||||
setIsAnnouncementsSidebarOpen(false);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
setActiveSlideoutPanel((prev) => (prev === "private" ? null : "private"));
|
||||
}, []);
|
||||
|
||||
// Delete handlers
|
||||
|
|
@ -741,9 +700,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
setTheme={setTheme}
|
||||
isChatPage={isChatPage}
|
||||
isLoadingChats={isLoadingThreads}
|
||||
activeSlideoutPanel={activeSlideoutPanel}
|
||||
onSlideoutPanelChange={setActiveSlideoutPanel}
|
||||
inbox={{
|
||||
isOpen: isInboxSidebarOpen,
|
||||
onOpenChange: setIsInboxSidebarOpen,
|
||||
totalUnreadCount,
|
||||
comments: {
|
||||
items: commentsInbox.inboxItems,
|
||||
|
|
@ -766,18 +726,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
markAllAsRead: statusInbox.markAllAsRead,
|
||||
},
|
||||
}}
|
||||
announcementsPanel={{
|
||||
open: isAnnouncementsSidebarOpen,
|
||||
onOpenChange: setIsAnnouncementsSidebarOpen,
|
||||
}}
|
||||
allSharedChatsPanel={{
|
||||
open: isAllSharedChatsSidebarOpen,
|
||||
onOpenChange: setIsAllSharedChatsSidebarOpen,
|
||||
searchSpaceId,
|
||||
}}
|
||||
allPrivateChatsPanel={{
|
||||
open: isAllPrivateChatsSidebarOpen,
|
||||
onOpenChange: setIsAllPrivateChatsSidebarOpen,
|
||||
searchSpaceId,
|
||||
}}
|
||||
documentsPanel={{
|
||||
|
|
@ -810,14 +762,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
disabled={isDeletingChat}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
||||
>
|
||||
{isDeletingChat ? (
|
||||
<>
|
||||
<Spinner size="sm" />
|
||||
{t("deleting")}
|
||||
</>
|
||||
) : (
|
||||
tCommon("delete")
|
||||
)}
|
||||
{isDeletingChat ? <Spinner size="sm" /> : tCommon("delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { PanelRight, PanelRightClose } from "lucide-react";
|
||||
import { startTransition, useEffect } from "react";
|
||||
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
||||
import { EditorPanelContent } from "@/components/editor-panel/editor-panel";
|
||||
import { HitlEditPanelContent } from "@/components/hitl-edit-panel/hitl-edit-panel";
|
||||
import { ReportPanelContent } from "@/components/report-panel/report-panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
|
@ -44,9 +46,11 @@ export function RightPanelExpandButton() {
|
|||
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
||||
const reportState = useAtomValue(reportPanelAtom);
|
||||
const editorState = useAtomValue(editorPanelAtom);
|
||||
const hitlEditState = useAtomValue(hitlEditPanelAtom);
|
||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||
const editorOpen = editorState.isOpen && !!editorState.documentId;
|
||||
const hasContent = documentsOpen || reportOpen || editorOpen;
|
||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen;
|
||||
|
||||
if (!collapsed || !hasContent) return null;
|
||||
|
||||
|
|
@ -70,7 +74,7 @@ export function RightPanelExpandButton() {
|
|||
);
|
||||
}
|
||||
|
||||
const PANEL_WIDTHS = { sources: 420, report: 640, editor: 640 } as const;
|
||||
const PANEL_WIDTHS = { sources: 420, report: 640, editor: 640, "hitl-edit": 640 } as const;
|
||||
|
||||
export function RightPanel({ documentsPanel }: RightPanelProps) {
|
||||
const [activeTab] = useAtom(rightPanelTabAtom);
|
||||
|
|
@ -78,33 +82,45 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
|||
const closeReport = useSetAtom(closeReportPanelAtom);
|
||||
const editorState = useAtomValue(editorPanelAtom);
|
||||
const closeEditor = useSetAtom(closeEditorPanelAtom);
|
||||
const hitlEditState = useAtomValue(hitlEditPanelAtom);
|
||||
const closeHitlEdit = useSetAtom(closeHitlEditPanelAtom);
|
||||
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
||||
|
||||
const documentsOpen = documentsPanel?.open ?? false;
|
||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||
const editorOpen = editorState.isOpen && !!editorState.documentId;
|
||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||
|
||||
useEffect(() => {
|
||||
if (!reportOpen && !editorOpen) return;
|
||||
if (!reportOpen && !editorOpen && !hitlEditOpen) return;
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
if (editorOpen) closeEditor();
|
||||
if (hitlEditOpen) closeHitlEdit();
|
||||
else if (editorOpen) closeEditor();
|
||||
else if (reportOpen) closeReport();
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [reportOpen, editorOpen, closeReport, closeEditor]);
|
||||
}, [reportOpen, editorOpen, hitlEditOpen, closeReport, closeEditor, closeHitlEdit]);
|
||||
|
||||
const isVisible = (documentsOpen || reportOpen || editorOpen) && !collapsed;
|
||||
const isVisible = (documentsOpen || reportOpen || editorOpen || hitlEditOpen) && !collapsed;
|
||||
|
||||
let effectiveTab = activeTab;
|
||||
if (effectiveTab === "editor" && !editorOpen) {
|
||||
if (effectiveTab === "hitl-edit" && !hitlEditOpen) {
|
||||
effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources";
|
||||
} else if (effectiveTab === "editor" && !editorOpen) {
|
||||
effectiveTab = reportOpen ? "report" : "sources";
|
||||
} else if (effectiveTab === "report" && !reportOpen) {
|
||||
effectiveTab = editorOpen ? "editor" : "sources";
|
||||
} else if (effectiveTab === "sources" && !documentsOpen) {
|
||||
effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources";
|
||||
effectiveTab = hitlEditOpen
|
||||
? "hitl-edit"
|
||||
: editorOpen
|
||||
? "editor"
|
||||
: reportOpen
|
||||
? "report"
|
||||
: "sources";
|
||||
}
|
||||
|
||||
const targetWidth = PANEL_WIDTHS[effectiveTab];
|
||||
|
|
@ -148,6 +164,19 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && (
|
||||
<div className="h-full flex flex-col">
|
||||
<HitlEditPanelContent
|
||||
title={hitlEditState.title}
|
||||
content={hitlEditState.content}
|
||||
toolName={hitlEditState.toolName}
|
||||
contentFormat={hitlEditState.contentFormat}
|
||||
extraFields={hitlEditState.extraFields}
|
||||
onSave={hitlEditState.onSave}
|
||||
onClose={closeHitlEdit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import type { InboxItem } from "@/hooks/use-inbox";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
|
|
@ -12,15 +13,16 @@ import { Header } from "../header";
|
|||
import { IconRail } from "../icon-rail";
|
||||
import { RightPanel } from "../right-panel/RightPanel";
|
||||
import {
|
||||
AllPrivateChatsSidebar,
|
||||
AllSharedChatsSidebar,
|
||||
AnnouncementsSidebar,
|
||||
AllPrivateChatsSidebarContent,
|
||||
AllSharedChatsSidebarContent,
|
||||
AnnouncementsSidebarContent,
|
||||
DocumentsSidebar,
|
||||
InboxSidebar,
|
||||
InboxSidebarContent,
|
||||
MobileSidebar,
|
||||
MobileSidebarTrigger,
|
||||
Sidebar,
|
||||
} from "../sidebar";
|
||||
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
||||
|
||||
// Per-tab data source
|
||||
interface TabDataSource {
|
||||
|
|
@ -34,10 +36,11 @@ interface TabDataSource {
|
|||
markAllAsRead: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | "announcements" | null;
|
||||
|
||||
// Inbox-related props — per-tab data sources with independent loading/pagination
|
||||
interface InboxProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
totalUnreadCount: number;
|
||||
comments: TabDataSource;
|
||||
status: TabDataSource;
|
||||
|
|
@ -75,22 +78,17 @@ interface LayoutShellProps {
|
|||
isChatPage?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
// Unified slide-out panel state
|
||||
activeSlideoutPanel?: ActiveSlideoutPanel;
|
||||
onSlideoutPanelChange?: (panel: ActiveSlideoutPanel) => void;
|
||||
// Inbox props
|
||||
inbox?: InboxProps;
|
||||
announcementsPanel?: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
isLoadingChats?: boolean;
|
||||
// All chats panel props
|
||||
allSharedChatsPanel?: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
};
|
||||
allPrivateChatsPanel?: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
};
|
||||
documentsPanel?: {
|
||||
|
|
@ -133,8 +131,9 @@ export function LayoutShell({
|
|||
isChatPage = false,
|
||||
children,
|
||||
className,
|
||||
activeSlideoutPanel = null,
|
||||
onSlideoutPanelChange,
|
||||
inbox,
|
||||
announcementsPanel,
|
||||
isLoadingChats = false,
|
||||
allSharedChatsPanel,
|
||||
allPrivateChatsPanel,
|
||||
|
|
@ -155,6 +154,26 @@ export function LayoutShell({
|
|||
[isCollapsed, setIsCollapsed, toggleCollapsed, sidebarWidth]
|
||||
);
|
||||
|
||||
const closeSlideout = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) onSlideoutPanelChange?.(null);
|
||||
},
|
||||
[onSlideoutPanelChange]
|
||||
);
|
||||
|
||||
const anySlideOutOpen = activeSlideoutPanel !== null;
|
||||
|
||||
const panelAriaLabel =
|
||||
activeSlideoutPanel === "inbox"
|
||||
? "Inbox"
|
||||
: activeSlideoutPanel === "shared"
|
||||
? "Shared Chats"
|
||||
: activeSlideoutPanel === "private"
|
||||
? "Private Chats"
|
||||
: activeSlideoutPanel === "announcements"
|
||||
? "Announcements"
|
||||
: "Panel";
|
||||
|
||||
// Mobile layout
|
||||
if (isMobile) {
|
||||
return (
|
||||
|
|
@ -171,8 +190,6 @@ export function LayoutShell({
|
|||
searchSpaces={searchSpaces}
|
||||
activeSearchSpaceId={activeSearchSpaceId}
|
||||
onSearchSpaceSelect={onSearchSpaceSelect}
|
||||
onSearchSpaceDelete={onSearchSpaceDelete}
|
||||
onSearchSpaceSettings={onSearchSpaceSettings}
|
||||
onAddSearchSpace={onAddSearchSpace}
|
||||
searchSpace={searchSpace}
|
||||
navItems={navItems}
|
||||
|
|
@ -187,8 +204,8 @@ export function LayoutShell({
|
|||
onChatArchive={onChatArchive}
|
||||
onViewAllSharedChats={onViewAllSharedChats}
|
||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||
isSharedChatsPanelOpen={allSharedChatsPanel?.open}
|
||||
isPrivateChatsPanelOpen={allPrivateChatsPanel?.open}
|
||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onManageMembers={onManageMembers}
|
||||
|
|
@ -204,66 +221,94 @@ export function LayoutShell({
|
|||
{children}
|
||||
</main>
|
||||
|
||||
{/* Mobile Inbox Sidebar - only render when open to avoid scroll blocking */}
|
||||
{inbox?.isOpen && (
|
||||
<InboxSidebar
|
||||
open={inbox.isOpen}
|
||||
onOpenChange={inbox.onOpenChange}
|
||||
comments={inbox.comments}
|
||||
status={inbox.status}
|
||||
totalUnreadCount={inbox.totalUnreadCount}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{/* Mobile unified slide-out panel */}
|
||||
<SidebarSlideOutPanel
|
||||
open={anySlideOutOpen}
|
||||
onOpenChange={closeSlideout}
|
||||
ariaLabel={panelAriaLabel}
|
||||
>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{activeSlideoutPanel === "inbox" && inbox && (
|
||||
<motion.div
|
||||
key="inbox"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<InboxSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
comments={inbox.comments}
|
||||
status={inbox.status}
|
||||
totalUnreadCount={inbox.totalUnreadCount}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "announcements" && (
|
||||
<motion.div
|
||||
key="announcements"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AnnouncementsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
||||
<motion.div
|
||||
key="shared"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllSharedChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
|
||||
<motion.div
|
||||
key="private"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllPrivateChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</SidebarSlideOutPanel>
|
||||
|
||||
{/* Mobile Documents Sidebar - slide-out panel */}
|
||||
{/* Mobile Documents Sidebar - separate (not part of slide-out group) */}
|
||||
{documentsPanel && (
|
||||
<DocumentsSidebar
|
||||
open={documentsPanel.open}
|
||||
onOpenChange={documentsPanel.onOpenChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile Announcements Sidebar */}
|
||||
{announcementsPanel?.open && (
|
||||
<AnnouncementsSidebar
|
||||
open={announcementsPanel.open}
|
||||
onOpenChange={announcementsPanel.onOpenChange}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile All Shared Chats - slide-out panel */}
|
||||
{allSharedChatsPanel && (
|
||||
<AllSharedChatsSidebar
|
||||
open={allSharedChatsPanel.open}
|
||||
onOpenChange={allSharedChatsPanel.onOpenChange}
|
||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile All Private Chats - slide-out panel */}
|
||||
{allPrivateChatsPanel && (
|
||||
<AllPrivateChatsSidebar
|
||||
open={allPrivateChatsPanel.open}
|
||||
onOpenChange={allPrivateChatsPanel.onOpenChange}
|
||||
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const anySlideOutOpen =
|
||||
inbox?.isOpen ||
|
||||
announcementsPanel?.open ||
|
||||
allSharedChatsPanel?.open ||
|
||||
allPrivateChatsPanel?.open;
|
||||
|
||||
// Desktop layout
|
||||
return (
|
||||
<SidebarProvider value={sidebarContextValue}>
|
||||
|
|
@ -305,8 +350,8 @@ export function LayoutShell({
|
|||
onChatArchive={onChatArchive}
|
||||
onViewAllSharedChats={onViewAllSharedChats}
|
||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||
isSharedChatsPanelOpen={allSharedChatsPanel?.open}
|
||||
isPrivateChatsPanelOpen={allPrivateChatsPanel?.open}
|
||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onManageMembers={onManageMembers}
|
||||
|
|
@ -324,39 +369,74 @@ export function LayoutShell({
|
|||
isResizing={isResizing}
|
||||
/>
|
||||
|
||||
{/* Slide-out panels render as siblings next to the sidebar */}
|
||||
{inbox && (
|
||||
<InboxSidebar
|
||||
open={inbox.isOpen}
|
||||
onOpenChange={inbox.onOpenChange}
|
||||
comments={inbox.comments}
|
||||
status={inbox.status}
|
||||
totalUnreadCount={inbox.totalUnreadCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{announcementsPanel && (
|
||||
<AnnouncementsSidebar
|
||||
open={announcementsPanel.open}
|
||||
onOpenChange={announcementsPanel.onOpenChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{allSharedChatsPanel && (
|
||||
<AllSharedChatsSidebar
|
||||
open={allSharedChatsPanel.open}
|
||||
onOpenChange={allSharedChatsPanel.onOpenChange}
|
||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{allPrivateChatsPanel && (
|
||||
<AllPrivateChatsSidebar
|
||||
open={allPrivateChatsPanel.open}
|
||||
onOpenChange={allPrivateChatsPanel.onOpenChange}
|
||||
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||
/>
|
||||
)}
|
||||
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
||||
<SidebarSlideOutPanel
|
||||
open={anySlideOutOpen}
|
||||
onOpenChange={closeSlideout}
|
||||
ariaLabel={panelAriaLabel}
|
||||
>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{activeSlideoutPanel === "inbox" && inbox && (
|
||||
<motion.div
|
||||
key="inbox"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<InboxSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
comments={inbox.comments}
|
||||
status={inbox.status}
|
||||
totalUnreadCount={inbox.totalUnreadCount}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "announcements" && (
|
||||
<motion.div
|
||||
key="announcements"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AnnouncementsSidebarContent onOpenChange={(open) => closeSlideout(open)} />
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
||||
<motion.div
|
||||
key="shared"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllSharedChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
|
||||
<motion.div
|
||||
key="private"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllPrivateChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</SidebarSlideOutPanel>
|
||||
</div>
|
||||
|
||||
{/* Resize handle — negative margins eat the flex gap so spacing stays unchanged */}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -50,19 +50,21 @@ import {
|
|||
import { cn } from "@/lib/utils";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
interface AllPrivateChatsSidebarProps {
|
||||
open: boolean;
|
||||
export interface AllPrivateChatsSidebarContentProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function AllPrivateChatsSidebar({
|
||||
open,
|
||||
interface AllPrivateChatsSidebarProps extends AllPrivateChatsSidebarContentProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export function AllPrivateChatsSidebarContent({
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllPrivateChatsSidebarProps) {
|
||||
}: AllPrivateChatsSidebarContentProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -96,16 +98,6 @@ export function AllPrivateChatsSidebar({
|
|||
|
||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [open, onOpenChange]);
|
||||
|
||||
const {
|
||||
data: threadsData,
|
||||
error: threadsError,
|
||||
|
|
@ -113,7 +105,7 @@ export function AllPrivateChatsSidebar({
|
|||
} = useQuery({
|
||||
queryKey: ["all-threads", searchSpaceId],
|
||||
queryFn: () => fetchThreads(Number(searchSpaceId)),
|
||||
enabled: !!searchSpaceId && open && !isSearchMode,
|
||||
enabled: !!searchSpaceId && !isSearchMode,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
@ -123,7 +115,7 @@ export function AllPrivateChatsSidebar({
|
|||
} = useQuery({
|
||||
queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery],
|
||||
queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()),
|
||||
enabled: !!searchSpaceId && open && isSearchMode,
|
||||
enabled: !!searchSpaceId && isSearchMode,
|
||||
});
|
||||
|
||||
// Filter to only private chats (PRIVATE visibility or no visibility set)
|
||||
|
|
@ -250,11 +242,7 @@ export function AllPrivateChatsSidebar({
|
|||
const archivedCount = archivedChats.length;
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
ariaLabel={t("chats") || "Private Chats"}
|
||||
>
|
||||
<>
|
||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{isMobile && (
|
||||
|
|
@ -530,6 +518,29 @@ export function AllPrivateChatsSidebar({
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AllPrivateChatsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllPrivateChatsSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
ariaLabel={t("chats") || "Private Chats"}
|
||||
>
|
||||
<AllPrivateChatsSidebarContent
|
||||
onOpenChange={onOpenChange}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -50,19 +50,21 @@ import {
|
|||
import { cn } from "@/lib/utils";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
interface AllSharedChatsSidebarProps {
|
||||
open: boolean;
|
||||
export interface AllSharedChatsSidebarContentProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function AllSharedChatsSidebar({
|
||||
open,
|
||||
interface AllSharedChatsSidebarProps extends AllSharedChatsSidebarContentProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export function AllSharedChatsSidebarContent({
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllSharedChatsSidebarProps) {
|
||||
}: AllSharedChatsSidebarContentProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -96,16 +98,6 @@ export function AllSharedChatsSidebar({
|
|||
|
||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [open, onOpenChange]);
|
||||
|
||||
const {
|
||||
data: threadsData,
|
||||
error: threadsError,
|
||||
|
|
@ -113,7 +105,7 @@ export function AllSharedChatsSidebar({
|
|||
} = useQuery({
|
||||
queryKey: ["all-threads", searchSpaceId],
|
||||
queryFn: () => fetchThreads(Number(searchSpaceId)),
|
||||
enabled: !!searchSpaceId && open && !isSearchMode,
|
||||
enabled: !!searchSpaceId && !isSearchMode,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
@ -123,7 +115,7 @@ export function AllSharedChatsSidebar({
|
|||
} = useQuery({
|
||||
queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery],
|
||||
queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()),
|
||||
enabled: !!searchSpaceId && open && isSearchMode,
|
||||
enabled: !!searchSpaceId && isSearchMode,
|
||||
});
|
||||
|
||||
// Filter to only shared chats (SEARCH_SPACE visibility)
|
||||
|
|
@ -250,11 +242,7 @@ export function AllSharedChatsSidebar({
|
|||
const archivedCount = archivedChats.length;
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
ariaLabel={t("shared_chats") || "Shared Chats"}
|
||||
>
|
||||
<>
|
||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{isMobile && (
|
||||
|
|
@ -530,6 +518,29 @@ export function AllSharedChatsSidebar({
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AllSharedChatsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllSharedChatsSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
ariaLabel={t("shared_chats") || "Shared Chats"}
|
||||
>
|
||||
<AllSharedChatsSidebarContent
|
||||
onOpenChange={onOpenChange}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,26 +9,27 @@ import { useAnnouncements } from "@/hooks/use-announcements";
|
|||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
interface AnnouncementsSidebarProps {
|
||||
open: boolean;
|
||||
export interface AnnouncementsSidebarContentProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function AnnouncementsSidebar({
|
||||
open,
|
||||
interface AnnouncementsSidebarProps extends AnnouncementsSidebarContentProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export function AnnouncementsSidebarContent({
|
||||
onOpenChange,
|
||||
onCloseMobileSidebar,
|
||||
}: AnnouncementsSidebarProps) {
|
||||
}: AnnouncementsSidebarContentProps) {
|
||||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
||||
const { announcements, markAllRead } = useAnnouncements();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
markAllRead();
|
||||
}, [open, markAllRead]);
|
||||
}, [markAllRead]);
|
||||
|
||||
const body = (
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -65,10 +66,19 @@ export function AnnouncementsSidebar({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnnouncementsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCloseMobileSidebar,
|
||||
}: AnnouncementsSidebarProps) {
|
||||
return (
|
||||
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel="Announcements">
|
||||
{body}
|
||||
<AnnouncementsSidebarContent
|
||||
onOpenChange={onOpenChange}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
||||
import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
||||
import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -43,7 +44,6 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import {
|
||||
|
|
@ -140,8 +140,7 @@ interface TabDataSource {
|
|||
markAllAsRead: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
interface InboxSidebarProps {
|
||||
open: boolean;
|
||||
export interface InboxSidebarContentProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
comments: TabDataSource;
|
||||
status: TabDataSource;
|
||||
|
|
@ -149,14 +148,17 @@ interface InboxSidebarProps {
|
|||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function InboxSidebar({
|
||||
open,
|
||||
interface InboxSidebarProps extends InboxSidebarContentProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export function InboxSidebarContent({
|
||||
onOpenChange,
|
||||
comments,
|
||||
status,
|
||||
totalUnreadCount,
|
||||
onCloseMobileSidebar,
|
||||
}: InboxSidebarProps) {
|
||||
}: InboxSidebarContentProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -199,7 +201,7 @@ export function InboxSidebar({
|
|||
},
|
||||
}),
|
||||
staleTime: 30 * 1000,
|
||||
enabled: isSearchMode && open,
|
||||
enabled: isSearchMode,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -207,23 +209,13 @@ export function InboxSidebar({
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [open, onOpenChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !isMobile) return;
|
||||
if (!isMobile) return;
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow;
|
||||
};
|
||||
}, [open, isMobile]);
|
||||
}, [isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== "status") {
|
||||
|
|
@ -239,7 +231,7 @@ export function InboxSidebar({
|
|||
queryKey: cacheKeys.notifications.sourceTypes(searchSpaceId),
|
||||
queryFn: () => notificationsApiService.getSourceTypes(searchSpaceId ?? undefined),
|
||||
staleTime: 60 * 1000,
|
||||
enabled: open && activeTab === "status",
|
||||
enabled: activeTab === "status",
|
||||
});
|
||||
|
||||
const statusSourceOptions = useMemo(() => {
|
||||
|
|
@ -327,7 +319,7 @@ export function InboxSidebar({
|
|||
|
||||
// Infinite scroll — uses active tab's pagination
|
||||
useEffect(() => {
|
||||
if (!activeSource.hasMore || activeSource.loadingMore || !open || isSearchMode) return;
|
||||
if (!activeSource.hasMore || activeSource.loadingMore || isSearchMode) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
|
|
@ -347,7 +339,7 @@ export function InboxSidebar({
|
|||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [activeSource.hasMore, activeSource.loadingMore, activeSource.loadMore, open, isSearchMode]);
|
||||
}, [activeSource.hasMore, activeSource.loadingMore, activeSource.loadMore, isSearchMode]);
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
async (item: InboxItem) => {
|
||||
|
|
@ -522,7 +514,7 @@ export function InboxSidebar({
|
|||
|
||||
const isLoading = isSearchMode ? isSearchLoading : activeSource.loading;
|
||||
|
||||
const inboxContent = (
|
||||
return (
|
||||
<>
|
||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -546,7 +538,7 @@ export function InboxSidebar({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={() => setFilterDrawerOpen(true)}
|
||||
>
|
||||
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
||||
|
|
@ -694,7 +686,7 @@ export function InboxSidebar({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 rounded-full">
|
||||
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">{t("filter") || "Filter"}</span>
|
||||
</Button>
|
||||
|
|
@ -790,7 +782,7 @@ export function InboxSidebar({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={handleMarkAllAsRead}
|
||||
disabled={totalUnreadCount === 0}
|
||||
>
|
||||
|
|
@ -803,7 +795,7 @@ export function InboxSidebar({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={handleMarkAllAsRead}
|
||||
disabled={totalUnreadCount === 0}
|
||||
>
|
||||
|
|
@ -851,14 +843,11 @@ export function InboxSidebar({
|
|||
setActiveFilter("all");
|
||||
}
|
||||
}}
|
||||
className="shrink-0 mx-4"
|
||||
className="shrink-0 mx-4 mt-2"
|
||||
>
|
||||
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
|
||||
<TabsTrigger
|
||||
value="comments"
|
||||
className="group flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||
>
|
||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted group-data-[state=active]:bg-muted transition-colors">
|
||||
<TabsList stretch showBottomBorder size="sm">
|
||||
<TabsTrigger value="comments">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span>{t("comments") || "Comments"}</span>
|
||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||
|
|
@ -866,11 +855,8 @@ export function InboxSidebar({
|
|||
</span>
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="status"
|
||||
className="group flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||
>
|
||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted group-data-[state=active]:bg-muted transition-colors">
|
||||
<TabsTrigger value="status">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<History className="h-4 w-4" />
|
||||
<span>{t("status") || "Status"}</span>
|
||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||
|
|
@ -885,9 +871,9 @@ export function InboxSidebar({
|
|||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{activeTab === "comments"
|
||||
? [85, 60, 90, 70, 50, 75].map((titleWidth, i) => (
|
||||
? [85, 60, 90, 70, 50, 75].map((titleWidth) => (
|
||||
<div
|
||||
key={`skeleton-comment-${i}`}
|
||||
key={`skeleton-comment-${titleWidth}`}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
|
||||
>
|
||||
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
|
||||
|
|
@ -898,9 +884,9 @@ export function InboxSidebar({
|
|||
<Skeleton className="h-3 w-6 shrink-0 rounded" />
|
||||
</div>
|
||||
))
|
||||
: [75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
|
||||
: [75, 90, 55, 80, 65, 85].map((titleWidth) => (
|
||||
<div
|
||||
key={`skeleton-status-${i}`}
|
||||
key={`skeleton-status-${titleWidth}`}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
|
||||
>
|
||||
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
|
||||
|
|
@ -1003,9 +989,9 @@ export function InboxSidebar({
|
|||
)}
|
||||
{activeSource.loadingMore &&
|
||||
(activeTab === "comments"
|
||||
? [80, 60, 90].map((titleWidth, i) => (
|
||||
? [80, 60, 90].map((titleWidth) => (
|
||||
<div
|
||||
key={`loading-more-comment-${i}`}
|
||||
key={`loading-more-comment-${titleWidth}`}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
|
||||
>
|
||||
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
|
||||
|
|
@ -1016,9 +1002,9 @@ export function InboxSidebar({
|
|||
<Skeleton className="h-3 w-6 shrink-0 rounded" />
|
||||
</div>
|
||||
))
|
||||
: [70, 85, 55].map((titleWidth, i) => (
|
||||
: [70, 85, 55].map((titleWidth) => (
|
||||
<div
|
||||
key={`loading-more-status-${i}`}
|
||||
key={`loading-more-status-${titleWidth}`}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
|
||||
>
|
||||
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
|
||||
|
|
@ -1057,10 +1043,27 @@ export function InboxSidebar({
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function InboxSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
comments,
|
||||
status,
|
||||
totalUnreadCount,
|
||||
onCloseMobileSidebar,
|
||||
}: InboxSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel={t("inbox") || "Inbox"}>
|
||||
{inboxContent}
|
||||
<InboxSidebarContent
|
||||
onOpenChange={onOpenChange}
|
||||
comments={comments}
|
||||
status={status}
|
||||
totalUnreadCount={totalUnreadCount}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import { CheckCircle2, CircleAlert } from "lucide-react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { NavItem } from "../../types/layout.types";
|
||||
import { SidebarButton } from "./SidebarButton";
|
||||
|
||||
interface NavSectionProps {
|
||||
items: NavItem[];
|
||||
|
|
@ -66,71 +66,49 @@ function StatusIcon({
|
|||
return <FallbackIcon className={cn("shrink-0", className)} />;
|
||||
}
|
||||
|
||||
function CollapsedOverlay({ item }: { item: NavItem }) {
|
||||
const indicator = item.statusIndicator;
|
||||
if (indicator && indicator !== "idle") {
|
||||
return <StatusBadge status={indicator} />;
|
||||
}
|
||||
if (item.badge) {
|
||||
return (
|
||||
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full bg-red-500 text-white text-[9px] font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function NavSection({ items, onItemClick, isCollapsed = false }: NavSectionProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const indicator = item.statusIndicator;
|
||||
|
||||
const joyrideAttr =
|
||||
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
|
||||
? { "data-joyride": "inbox-sidebar" }
|
||||
? { "data-joyride": "inbox-sidebar" as const }
|
||||
: {};
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<Tooltip key={item.url}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onItemClick?.(item)}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
)}
|
||||
{...joyrideAttr}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{indicator && indicator !== "idle" ? (
|
||||
<StatusBadge status={indicator} />
|
||||
) : item.badge ? (
|
||||
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full bg-red-500 text-white text-[9px] font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="sr-only">{item.title}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{item.title}
|
||||
{item.badge && ` (${item.badge})`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
<SidebarButton
|
||||
key={item.url}
|
||||
type="button"
|
||||
icon={item.icon}
|
||||
label={item.title}
|
||||
onClick={() => onItemClick?.(item)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
)}
|
||||
{...joyrideAttr}
|
||||
>
|
||||
<StatusIcon status={indicator} FallbackIcon={Icon} className="h-4 w-4" />
|
||||
<span className="flex-1 truncate">{item.title}</span>
|
||||
{item.badge && (
|
||||
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
isCollapsed={isCollapsed}
|
||||
isActive={item.isActive}
|
||||
badge={item.badge}
|
||||
collapsedOverlay={<CollapsedOverlay item={item} />}
|
||||
expandedIconNode={
|
||||
<StatusIcon
|
||||
status={item.statusIndicator}
|
||||
FallbackIcon={item.icon}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
buttonProps={joyrideAttr}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@
|
|||
import { PenSquare } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||
import { ChatListItem } from "./ChatListItem";
|
||||
import { NavSection } from "./NavSection";
|
||||
import { PageUsageDisplay } from "./PageUsageDisplay";
|
||||
import { SidebarButton } from "./SidebarButton";
|
||||
import { SidebarCollapseButton } from "./SidebarCollapseButton";
|
||||
import { SidebarHeader } from "./SidebarHeader";
|
||||
import { SidebarSection } from "./SidebarSection";
|
||||
|
|
@ -132,23 +131,13 @@ export function Sidebar({
|
|||
)}
|
||||
|
||||
{/* New chat button */}
|
||||
<div className="p-2">
|
||||
{isCollapsed ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="w-full h-10" onClick={onNewChat}>
|
||||
<PenSquare className="h-4 w-4" />
|
||||
<span className="sr-only">{t("new_chat")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{t("new_chat")}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant="outline" className="w-full justify-start gap-2" onClick={onNewChat}>
|
||||
<PenSquare className="h-4 w-4" />
|
||||
{t("new_chat")}
|
||||
</Button>
|
||||
)}
|
||||
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
||||
<SidebarButton
|
||||
icon={PenSquare}
|
||||
label={t("new_chat")}
|
||||
onClick={onNewChat}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chat sections - fills available space */}
|
||||
|
|
@ -168,7 +157,7 @@ export function Sidebar({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onViewAllSharedChats}
|
||||
className="text-[10px] text-muted-foreground/70 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
|
||||
className="text-xs font-medium text-muted-foreground/60 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
|
||||
>
|
||||
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
|
||||
</button>
|
||||
|
|
@ -224,7 +213,7 @@ export function Sidebar({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onViewAllPrivateChats}
|
||||
className="text-[10px] text-muted-foreground/70 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
|
||||
className="text-xs font-medium text-muted-foreground/60 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
|
||||
>
|
||||
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")}
|
||||
</button>
|
||||
|
|
|
|||
90
surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
Normal file
90
surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"use client";
|
||||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SidebarButtonProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
isCollapsed?: boolean;
|
||||
isActive?: boolean;
|
||||
badge?: React.ReactNode;
|
||||
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
|
||||
collapsedOverlay?: React.ReactNode;
|
||||
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
|
||||
expandedIconNode?: React.ReactNode;
|
||||
className?: string;
|
||||
/** Extra attributes spread onto the inner <button> (e.g. data-joyride) */
|
||||
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const expandedClassName = cn(
|
||||
"flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
);
|
||||
|
||||
const collapsedClassName = cn(
|
||||
"relative flex h-10 w-10 items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
);
|
||||
|
||||
export function SidebarButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
isCollapsed = false,
|
||||
isActive = false,
|
||||
badge,
|
||||
collapsedOverlay,
|
||||
expandedIconNode,
|
||||
className,
|
||||
buttonProps,
|
||||
}: SidebarButtonProps) {
|
||||
const activeClassName = "bg-accent text-accent-foreground";
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(collapsedClassName, isActive && activeClassName, className)}
|
||||
{...buttonProps}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{collapsedOverlay}
|
||||
<span className="sr-only">{label}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{label}
|
||||
{typeof badge === "string" && ` (${badge})`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(expandedClassName, isActive && activeClassName, className)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{expandedIconNode ?? <Icon className="h-4 w-4 shrink-0" />}
|
||||
<span className="flex-1 truncate">{label}</span>
|
||||
{badge && typeof badge !== "string" ? badge : null}
|
||||
{badge && typeof badge === "string" ? (
|
||||
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
export { AllPrivateChatsSidebar } from "./AllPrivateChatsSidebar";
|
||||
export { AllSharedChatsSidebar } from "./AllSharedChatsSidebar";
|
||||
export { AnnouncementsSidebar } from "./AnnouncementsSidebar";
|
||||
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
|
||||
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
|
||||
export { AnnouncementsSidebar, AnnouncementsSidebarContent } from "./AnnouncementsSidebar";
|
||||
export { ChatListItem } from "./ChatListItem";
|
||||
export { DocumentsSidebar } from "./DocumentsSidebar";
|
||||
export { InboxSidebar } from "./InboxSidebar";
|
||||
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
|
||||
export { MobileSidebar, MobileSidebarTrigger } from "./MobileSidebar";
|
||||
export { NavSection } from "./NavSection";
|
||||
export { PageUsageDisplay } from "./PageUsageDisplay";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue