mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
feat: implement macOS-specific title bar adjustments and enhance RightPanel with toggle functionality
This commit is contained in:
parent
ee3a6dc45f
commit
cd4e5ae7f2
5 changed files with 322 additions and 200 deletions
|
|
@ -7,6 +7,7 @@ import { setActiveSearchSpaceId } from './active-search-space';
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string;
|
const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string;
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let isQuitting = false;
|
let isQuitting = false;
|
||||||
|
|
@ -35,7 +36,12 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
||||||
webviewTag: false,
|
webviewTag: false,
|
||||||
},
|
},
|
||||||
show: false,
|
show: false,
|
||||||
titleBarStyle: 'hiddenInset',
|
...(isMac
|
||||||
|
? {
|
||||||
|
titleBarStyle: 'hidden' as const,
|
||||||
|
trafficLightPosition: { x: 12, y: 10 },
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { PanelRight } from "lucide-react";
|
import { PanelRight } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { startTransition, useEffect, type MouseEvent } from "react";
|
import { type MouseEvent, startTransition, useEffect } from "react";
|
||||||
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
|
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
|
||||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
|
|
@ -12,6 +12,7 @@ import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl";
|
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { DocumentsSidebar } from "../sidebar";
|
import { DocumentsSidebar } from "../sidebar";
|
||||||
|
|
||||||
const EditorPanelContent = dynamic(
|
const EditorPanelContent = dynamic(
|
||||||
|
|
@ -51,6 +52,8 @@ interface RightPanelProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
showCollapseButton?: boolean;
|
||||||
|
showTopBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKeyboardClick(event: MouseEvent) {
|
function isKeyboardClick(event: MouseEvent) {
|
||||||
|
|
@ -80,13 +83,66 @@ function CollapseButton({ onClick }: { onClick: () => void }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RightPanelToggleButtonProps {
|
||||||
|
className?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RightPanelToggleButton({
|
||||||
|
className,
|
||||||
|
iconClassName,
|
||||||
|
disabled = false,
|
||||||
|
}: RightPanelToggleButtonProps) {
|
||||||
|
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
||||||
|
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
||||||
|
const reportState = useAtomValue(reportPanelAtom);
|
||||||
|
const editorState = useAtomValue(editorPanelAtom);
|
||||||
|
const hitlEditState = useAtomValue(hitlEditPanelAtom);
|
||||||
|
const citationState = useAtomValue(citationPanelAtom);
|
||||||
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
|
const editorOpen =
|
||||||
|
editorState.isOpen &&
|
||||||
|
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
|
||||||
|
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||||
|
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||||
|
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
||||||
|
const label = collapsed ? "Expand panel" : "Collapse panel";
|
||||||
|
|
||||||
|
if (!hasContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (disabled) return;
|
||||||
|
startTransition(() => setCollapsed((value) => !value));
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PanelRight className={cn("h-4 w-4", iconClassName)} />
|
||||||
|
<span className="sr-only">{label}</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Absolutely positioned expand button — renders at top-right of the main
|
* Absolutely positioned expand button — renders at top-right of the main
|
||||||
* container so it occupies the same screen position as the collapse button
|
* container so it occupies the same screen position as the collapse button
|
||||||
* inside the Documents header.
|
* inside the Documents header.
|
||||||
*/
|
*/
|
||||||
export function RightPanelExpandButton() {
|
export function RightPanelExpandButton() {
|
||||||
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
const [collapsed] = useAtom(rightPanelCollapsedAtom);
|
||||||
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
||||||
const reportState = useAtomValue(reportPanelAtom);
|
const reportState = useAtomValue(reportPanelAtom);
|
||||||
const editorState = useAtomValue(editorPanelAtom);
|
const editorState = useAtomValue(editorPanelAtom);
|
||||||
|
|
@ -104,24 +160,7 @@ export function RightPanelExpandButton() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex shrink-0 items-center px-0.5">
|
<div className="flex shrink-0 items-center px-0.5">
|
||||||
<Tooltip>
|
<RightPanelToggleButton className="-m-0.5" />
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (isKeyboardClick(event)) return;
|
|
||||||
startTransition(() => setCollapsed(false));
|
|
||||||
}}
|
|
||||||
className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<PanelRight className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Expand panel</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Expand panel</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +173,11 @@ const PANEL_WIDTHS = {
|
||||||
citation: 560,
|
citation: 560,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function RightPanel({ documentsPanel }: RightPanelProps) {
|
export function RightPanel({
|
||||||
|
documentsPanel,
|
||||||
|
showCollapseButton = true,
|
||||||
|
showTopBorder = false,
|
||||||
|
}: RightPanelProps) {
|
||||||
const [activeTab] = useAtom(rightPanelTabAtom);
|
const [activeTab] = useAtom(rightPanelTabAtom);
|
||||||
const reportState = useAtomValue(reportPanelAtom);
|
const reportState = useAtomValue(reportPanelAtom);
|
||||||
const closeReport = useSetAtom(closeReportPanelAtom);
|
const closeReport = useSetAtom(closeReportPanelAtom);
|
||||||
|
|
@ -208,14 +251,19 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetWidth = PANEL_WIDTHS[effectiveTab];
|
const targetWidth = PANEL_WIDTHS[effectiveTab];
|
||||||
const collapseButton = <CollapseButton onClick={() => setCollapsed(true)} />;
|
const collapseButton = showCollapseButton ? (
|
||||||
|
<CollapseButton onClick={() => setCollapsed(true)} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
style={{ width: targetWidth }}
|
style={{ width: targetWidth }}
|
||||||
className="flex h-full shrink-0 flex-col border-l bg-panel text-sidebar-foreground overflow-hidden transition-[width] duration-200 ease-out"
|
className={cn(
|
||||||
|
"flex h-full shrink-0 flex-col border-l bg-panel text-sidebar-foreground overflow-hidden transition-[width] duration-200 ease-out",
|
||||||
|
showTopBorder && "border-t"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||||
{effectiveTab === "sources" && documentsOpen && documentsPanel && (
|
{effectiveTab === "sources" && documentsOpen && documentsPanel && (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { Spinner } from "@/components/ui/spinner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import type { InboxItem } from "@/hooks/use-inbox";
|
import type { InboxItem } from "@/hooks/use-inbox";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
import { useElectronAPI } from "@/hooks/use-platform";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SidebarProvider, useSidebarState } from "../../hooks";
|
import { SidebarProvider, useSidebarState } from "../../hooks";
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,7 +20,11 @@ import {
|
||||||
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 { RightPanel, RightPanelExpandButton } from "../right-panel/RightPanel";
|
import {
|
||||||
|
RightPanel,
|
||||||
|
RightPanelExpandButton,
|
||||||
|
RightPanelToggleButton,
|
||||||
|
} from "../right-panel/RightPanel";
|
||||||
import {
|
import {
|
||||||
AllPrivateChatsSidebarContent,
|
AllPrivateChatsSidebarContent,
|
||||||
AllSharedChatsSidebarContent,
|
AllSharedChatsSidebarContent,
|
||||||
|
|
@ -28,6 +33,7 @@ import {
|
||||||
MobileSidebar,
|
MobileSidebar,
|
||||||
MobileSidebarTrigger,
|
MobileSidebarTrigger,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
SidebarCollapseButton,
|
||||||
} from "../sidebar";
|
} from "../sidebar";
|
||||||
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
||||||
import { TabBar } from "../tabs/TabBar";
|
import { TabBar } from "../tabs/TabBar";
|
||||||
|
|
@ -45,6 +51,36 @@ const DocumentTabContent = dynamic(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function MacDesktopTitleBar({
|
||||||
|
isSidebarCollapsed,
|
||||||
|
onToggleSidebar,
|
||||||
|
disableRightPanelToggle = false,
|
||||||
|
}: {
|
||||||
|
isSidebarCollapsed: boolean;
|
||||||
|
onToggleSidebar: () => void;
|
||||||
|
disableRightPanelToggle?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-9 shrink-0 items-center bg-rail px-2 [app-region:drag] [-webkit-app-region:drag]">
|
||||||
|
<div className="ml-[72px] flex h-full items-center [app-region:no-drag] [-webkit-app-region:no-drag]">
|
||||||
|
<SidebarCollapseButton
|
||||||
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
onToggle={onToggleSidebar}
|
||||||
|
className="h-6 w-6 rounded-md"
|
||||||
|
iconClassName="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex h-full items-center [app-region:no-drag] [-webkit-app-region:no-drag]">
|
||||||
|
<RightPanelToggleButton
|
||||||
|
disabled={disableRightPanelToggle}
|
||||||
|
className="h-6 w-6 rounded-md"
|
||||||
|
iconClassName="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Per-tab data source
|
// Per-tab data source
|
||||||
interface TabDataSource {
|
interface TabDataSource {
|
||||||
items: InboxItem[];
|
items: InboxItem[];
|
||||||
|
|
@ -130,22 +166,28 @@ function MainContentPanel({
|
||||||
isChatPage,
|
isChatPage,
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
|
showRightPanelExpandButton = true,
|
||||||
|
showTopBorder = false,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
isChatPage: boolean;
|
isChatPage: boolean;
|
||||||
onTabSwitch?: (tab: Tab) => void;
|
onTabSwitch?: (tab: Tab) => void;
|
||||||
onNewChat?: () => void;
|
onNewChat?: () => void;
|
||||||
|
showRightPanelExpandButton?: boolean;
|
||||||
|
showTopBorder?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const activeTab = useAtomValue(activeTabAtom);
|
const activeTab = useAtomValue(activeTabAtom);
|
||||||
const isDocumentTab = activeTab?.type === "document";
|
const isDocumentTab = activeTab?.type === "document";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative isolate flex flex-1 flex-col min-w-0">
|
<div
|
||||||
|
className={cn("relative isolate flex flex-1 flex-col min-w-0", showTopBorder && "border-t")}
|
||||||
|
>
|
||||||
<TabBar
|
<TabBar
|
||||||
onTabSwitch={onTabSwitch}
|
onTabSwitch={onTabSwitch}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
rightActions={<RightPanelExpandButton />}
|
rightActions={showRightPanelExpandButton ? <RightPanelExpandButton /> : null}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
/>
|
/>
|
||||||
<div className="relative flex flex-1 flex-col bg-panel overflow-hidden min-w-0">
|
<div className="relative flex flex-1 flex-col bg-panel overflow-hidden min-w-0">
|
||||||
|
|
@ -221,6 +263,8 @@ export function LayoutShell({
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
}: LayoutShellProps) {
|
}: LayoutShellProps) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const electronAPI = useElectronAPI();
|
||||||
|
const isMacDesktop = electronAPI?.versions.platform === "darwin";
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed);
|
const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed);
|
||||||
const {
|
const {
|
||||||
|
|
@ -388,175 +432,190 @@ export function LayoutShell({
|
||||||
return (
|
return (
|
||||||
<SidebarProvider value={sidebarContextValue}>
|
<SidebarProvider value={sidebarContextValue}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div
|
<div className={cn("flex h-screen w-full flex-col overflow-hidden bg-rail", className)}>
|
||||||
className={cn(
|
{isMacDesktop ? (
|
||||||
"flex h-screen w-full gap-2 px-2 py-0 overflow-hidden bg-rail",
|
<MacDesktopTitleBar
|
||||||
className
|
isSidebarCollapsed={isCollapsed}
|
||||||
)}
|
onToggleSidebar={toggleCollapsed}
|
||||||
>
|
disableRightPanelToggle={useWorkspacePanel}
|
||||||
<div className="hidden md:flex overflow-hidden border-r -mr-2 pr-2 bg-rail">
|
|
||||||
<IconRail
|
|
||||||
searchSpaces={searchSpaces}
|
|
||||||
activeSearchSpaceId={activeSearchSpaceId}
|
|
||||||
onSearchSpaceSelect={onSearchSpaceSelect}
|
|
||||||
onSearchSpaceDelete={onSearchSpaceDelete}
|
|
||||||
onSearchSpaceSettings={onSearchSpaceSettings}
|
|
||||||
onAddSearchSpace={onAddSearchSpace}
|
|
||||||
isSingleRailMode={false}
|
|
||||||
user={user}
|
|
||||||
onUserSettings={onUserSettings}
|
|
||||||
onAnnouncements={onAnnouncements}
|
|
||||||
announcementUnreadCount={announcementUnreadCount}
|
|
||||||
onLogout={onLogout}
|
|
||||||
theme={theme}
|
|
||||||
setTheme={setTheme}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : null}
|
||||||
|
<div className="flex min-h-0 flex-1 w-full gap-2 px-2 py-0 overflow-hidden">
|
||||||
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content. Negative right margin closes the flex gap so the sidebar sits flush against the main panel, separated only by a border. */}
|
<div className="hidden md:flex overflow-hidden -mr-2 pr-2 bg-rail">
|
||||||
<div className="relative hidden md:flex shrink-0 z-20 -mr-2 border-r bg-panel">
|
<IconRail
|
||||||
<Sidebar
|
searchSpaces={searchSpaces}
|
||||||
searchSpace={searchSpace}
|
activeSearchSpaceId={activeSearchSpaceId}
|
||||||
isCollapsed={isCollapsed}
|
onSearchSpaceSelect={onSearchSpaceSelect}
|
||||||
onToggleCollapse={toggleCollapsed}
|
onSearchSpaceDelete={onSearchSpaceDelete}
|
||||||
navItems={navItems}
|
onSearchSpaceSettings={onSearchSpaceSettings}
|
||||||
onNavItemClick={onNavItemClick}
|
onAddSearchSpace={onAddSearchSpace}
|
||||||
chats={chats}
|
isSingleRailMode={false}
|
||||||
sharedChats={sharedChats}
|
user={user}
|
||||||
activeChatId={activeChatId}
|
onUserSettings={onUserSettings}
|
||||||
onNewChat={onNewChat}
|
onAnnouncements={onAnnouncements}
|
||||||
onChatSelect={onChatSelect}
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onChatRename={onChatRename}
|
onLogout={onLogout}
|
||||||
onChatDelete={onChatDelete}
|
theme={theme}
|
||||||
onChatArchive={onChatArchive}
|
setTheme={setTheme}
|
||||||
onViewAllSharedChats={onViewAllSharedChats}
|
|
||||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
|
||||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
|
||||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
|
||||||
user={user}
|
|
||||||
onSettings={onSettings}
|
|
||||||
onManageMembers={onManageMembers}
|
|
||||||
onUserSettings={onUserSettings}
|
|
||||||
onAnnouncements={onAnnouncements}
|
|
||||||
announcementUnreadCount={announcementUnreadCount}
|
|
||||||
onLogout={onLogout}
|
|
||||||
pageUsage={pageUsage}
|
|
||||||
theme={theme}
|
|
||||||
setTheme={setTheme}
|
|
||||||
renderUserProfile={false}
|
|
||||||
className="flex shrink-0"
|
|
||||||
isLoadingChats={isLoadingChats}
|
|
||||||
sidebarWidth={sidebarWidth}
|
|
||||||
isResizing={isResizing}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isCollapsed && (
|
|
||||||
<hr
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-label="Resize sidebar"
|
|
||||||
aria-valuemin={SIDEBAR_MIN_WIDTH}
|
|
||||||
aria-valuemax={SIDEBAR_MAX_WIDTH}
|
|
||||||
aria-valuenow={sidebarWidth}
|
|
||||||
tabIndex={0}
|
|
||||||
onPointerDown={onResizePointerDown}
|
|
||||||
style={{ touchAction: "none" }}
|
|
||||||
className={cn(
|
|
||||||
"absolute top-0 right-0 h-full w-4 translate-x-1/2 z-50 m-0 border-0 bg-transparent p-0 select-none cursor-col-resize",
|
|
||||||
"after:content-[''] after:absolute after:inset-y-0 after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-transparent hover:after:bg-border/80 after:transition-colors",
|
|
||||||
isResizing && "after:bg-border"
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content. Negative right margin closes the flex gap so the sidebar sits flush against the main panel, separated only by a border. */}
|
||||||
<SidebarSlideOutPanel
|
<div
|
||||||
open={anySlideOutOpen}
|
className={cn(
|
||||||
onOpenChange={closeSlideout}
|
"relative hidden md:flex shrink-0 z-20 -mr-2 bg-panel",
|
||||||
ariaLabel={panelAriaLabel}
|
isMacDesktop ? "rounded-tl-xl border-t border-r border-l" : "border-r"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
<Sidebar
|
||||||
{activeSlideoutPanel === "inbox" && inbox && (
|
searchSpace={searchSpace}
|
||||||
<motion.div
|
isCollapsed={isCollapsed}
|
||||||
key="inbox"
|
onToggleCollapse={toggleCollapsed}
|
||||||
className="h-full flex flex-col"
|
navItems={navItems}
|
||||||
initial={{ opacity: 0 }}
|
onNavItemClick={onNavItemClick}
|
||||||
animate={{ opacity: 1 }}
|
chats={chats}
|
||||||
exit={{ opacity: 0 }}
|
sharedChats={sharedChats}
|
||||||
transition={{ duration: 0.15 }}
|
activeChatId={activeChatId}
|
||||||
>
|
onNewChat={onNewChat}
|
||||||
<InboxSidebarContent
|
onChatSelect={onChatSelect}
|
||||||
onOpenChange={(open) => closeSlideout(open)}
|
onChatRename={onChatRename}
|
||||||
comments={inbox.comments}
|
onChatDelete={onChatDelete}
|
||||||
status={inbox.status}
|
onChatArchive={onChatArchive}
|
||||||
totalUnreadCount={inbox.totalUnreadCount}
|
onViewAllSharedChats={onViewAllSharedChats}
|
||||||
/>
|
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||||
</motion.div>
|
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
||||||
)}
|
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
||||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
user={user}
|
||||||
<motion.div
|
onSettings={onSettings}
|
||||||
key="shared"
|
onManageMembers={onManageMembers}
|
||||||
className="h-full flex flex-col"
|
onUserSettings={onUserSettings}
|
||||||
initial={{ opacity: 0 }}
|
onAnnouncements={onAnnouncements}
|
||||||
animate={{ opacity: 1 }}
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
exit={{ opacity: 0 }}
|
onLogout={onLogout}
|
||||||
transition={{ duration: 0.15 }}
|
pageUsage={pageUsage}
|
||||||
>
|
theme={theme}
|
||||||
<AllSharedChatsSidebarContent
|
setTheme={setTheme}
|
||||||
onOpenChange={(open) => closeSlideout(open)}
|
renderUserProfile={false}
|
||||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
renderCollapseButton={!isMacDesktop}
|
||||||
/>
|
className={cn("flex shrink-0", isMacDesktop && "rounded-tl-xl")}
|
||||||
</motion.div>
|
isLoadingChats={isLoadingChats}
|
||||||
)}
|
sidebarWidth={sidebarWidth}
|
||||||
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
|
isResizing={isResizing}
|
||||||
<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>
|
|
||||||
|
|
||||||
<DesktopWorkspaceRegion>
|
{!isCollapsed && (
|
||||||
{useWorkspacePanel ? (
|
<hr
|
||||||
<WorkspacePanel
|
aria-orientation="vertical"
|
||||||
viewportClassName={workspacePanelViewportClassName}
|
aria-label="Resize sidebar"
|
||||||
contentClassName={workspacePanelContentClassName}
|
aria-valuemin={SIDEBAR_MIN_WIDTH}
|
||||||
|
aria-valuemax={SIDEBAR_MAX_WIDTH}
|
||||||
|
aria-valuenow={sidebarWidth}
|
||||||
|
tabIndex={0}
|
||||||
|
onPointerDown={onResizePointerDown}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0 right-0 h-full w-4 translate-x-1/2 z-50 m-0 border-0 bg-transparent p-0 select-none cursor-col-resize",
|
||||||
|
"after:content-[''] after:absolute after:inset-y-0 after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-transparent hover:after:bg-border/80 after:transition-colors",
|
||||||
|
isResizing && "after:bg-border"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
||||||
|
<SidebarSlideOutPanel
|
||||||
|
open={anySlideOutOpen}
|
||||||
|
onOpenChange={closeSlideout}
|
||||||
|
ariaLabel={panelAriaLabel}
|
||||||
>
|
>
|
||||||
{children}
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
</WorkspacePanel>
|
{activeSlideoutPanel === "inbox" && inbox && (
|
||||||
) : (
|
<motion.div
|
||||||
<>
|
key="inbox"
|
||||||
{/* Main content panel */}
|
className="h-full flex flex-col"
|
||||||
<MainContentPanel
|
initial={{ opacity: 0 }}
|
||||||
isChatPage={isChatPage}
|
animate={{ opacity: 1 }}
|
||||||
onTabSwitch={onTabSwitch}
|
exit={{ opacity: 0 }}
|
||||||
onNewChat={onNewChat}
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
<InboxSidebarContent
|
||||||
|
onOpenChange={(open) => closeSlideout(open)}
|
||||||
|
comments={inbox.comments}
|
||||||
|
status={inbox.status}
|
||||||
|
totalUnreadCount={inbox.totalUnreadCount}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<DesktopWorkspaceRegion>
|
||||||
|
{useWorkspacePanel ? (
|
||||||
|
<WorkspacePanel
|
||||||
|
className={isMacDesktop ? "border-t" : undefined}
|
||||||
|
viewportClassName={workspacePanelViewportClassName}
|
||||||
|
contentClassName={workspacePanelContentClassName}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MainContentPanel>
|
</WorkspacePanel>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Main content panel */}
|
||||||
|
<MainContentPanel
|
||||||
|
isChatPage={isChatPage}
|
||||||
|
onTabSwitch={onTabSwitch}
|
||||||
|
onNewChat={onNewChat}
|
||||||
|
showRightPanelExpandButton={!isMacDesktop}
|
||||||
|
showTopBorder={isMacDesktop}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MainContentPanel>
|
||||||
|
|
||||||
{/* Right panel — tabbed Sources/Report (desktop only) */}
|
{/* Right panel — tabbed Sources/Report (desktop only) */}
|
||||||
{documentsPanel ? (
|
{documentsPanel ? (
|
||||||
<RightPanel
|
<RightPanel
|
||||||
documentsPanel={{
|
documentsPanel={{
|
||||||
open: documentsPanel.open,
|
open: documentsPanel.open,
|
||||||
onOpenChange: documentsPanel.onOpenChange,
|
onOpenChange: documentsPanel.onOpenChange,
|
||||||
}}
|
}}
|
||||||
/>
|
showCollapseButton={!isMacDesktop}
|
||||||
) : null}
|
showTopBorder={isMacDesktop}
|
||||||
</>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</DesktopWorkspaceRegion>
|
</>
|
||||||
|
)}
|
||||||
|
</DesktopWorkspaceRegion>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ interface SidebarProps {
|
||||||
sidebarWidth?: number;
|
sidebarWidth?: number;
|
||||||
isResizing?: boolean;
|
isResizing?: boolean;
|
||||||
renderUserProfile?: boolean;
|
renderUserProfile?: boolean;
|
||||||
|
renderCollapseButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
|
|
@ -132,6 +133,7 @@ export function Sidebar({
|
||||||
sidebarWidth = SIDEBAR_MIN_WIDTH,
|
sidebarWidth = SIDEBAR_MIN_WIDTH,
|
||||||
isResizing = false,
|
isResizing = false,
|
||||||
renderUserProfile = true,
|
renderUserProfile = true,
|
||||||
|
renderCollapseButton = true,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
||||||
|
|
@ -176,13 +178,15 @@ export function Sidebar({
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("shrink-0", isCollapsed && "mx-auto")}>
|
{renderCollapseButton ? (
|
||||||
<SidebarCollapseButton
|
<div className={cn("shrink-0", isCollapsed && "mx-auto")}>
|
||||||
isCollapsed={isCollapsed}
|
<SidebarCollapseButton
|
||||||
onToggle={onToggleCollapse ?? (() => {})}
|
isCollapsed={isCollapsed}
|
||||||
disableTooltip={disableTooltips}
|
onToggle={onToggleCollapse ?? (() => {})}
|
||||||
/>
|
disableTooltip={disableTooltips}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-0.5 py-1.5">
|
<div className="flex flex-col gap-0.5 py-1.5">
|
||||||
|
|
@ -383,10 +387,7 @@ function SidebarUsageFooter({
|
||||||
|
|
||||||
if (isCollapsed) return null;
|
if (isCollapsed) return null;
|
||||||
|
|
||||||
const containerClass = cn(
|
const containerClass = cn("px-3 py-3 space-y-3", hasNavSectionAbove && "border-t");
|
||||||
"px-3 py-3 space-y-3",
|
|
||||||
hasNavSectionAbove && "border-t"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,22 @@ import { Button } from "@/components/ui/button";
|
||||||
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
|
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { usePlatformShortcut } from "@/hooks/use-platform-shortcut";
|
import { usePlatformShortcut } from "@/hooks/use-platform-shortcut";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface SidebarCollapseButtonProps {
|
interface SidebarCollapseButtonProps {
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
disableTooltip?: boolean;
|
disableTooltip?: boolean;
|
||||||
|
className?: string;
|
||||||
|
iconClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarCollapseButton({
|
export function SidebarCollapseButton({
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
onToggle,
|
onToggle,
|
||||||
disableTooltip = false,
|
disableTooltip = false,
|
||||||
|
className,
|
||||||
|
iconClassName,
|
||||||
}: SidebarCollapseButtonProps) {
|
}: SidebarCollapseButtonProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const { shortcutKeys } = usePlatformShortcut();
|
const { shortcutKeys } = usePlatformShortcut();
|
||||||
|
|
@ -26,9 +31,12 @@ export function SidebarCollapseButton({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
className={cn(
|
||||||
|
"h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<PanelLeft className="h-4 w-4" />
|
<PanelLeft className={cn("h-4 w-4", iconClassName)} />
|
||||||
<span className="sr-only">{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}</span>
|
<span className="sr-only">{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue