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
|
|
@ -9,6 +9,7 @@ import { Spinner } from "@/components/ui/spinner";
|
|||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import type { InboxItem } from "@/hooks/use-inbox";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarProvider, useSidebarState } from "../../hooks";
|
||||
import {
|
||||
|
|
@ -19,7 +20,11 @@ import {
|
|||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||
import { Header } from "../header";
|
||||
import { IconRail } from "../icon-rail";
|
||||
import { RightPanel, RightPanelExpandButton } from "../right-panel/RightPanel";
|
||||
import {
|
||||
RightPanel,
|
||||
RightPanelExpandButton,
|
||||
RightPanelToggleButton,
|
||||
} from "../right-panel/RightPanel";
|
||||
import {
|
||||
AllPrivateChatsSidebarContent,
|
||||
AllSharedChatsSidebarContent,
|
||||
|
|
@ -28,6 +33,7 @@ import {
|
|||
MobileSidebar,
|
||||
MobileSidebarTrigger,
|
||||
Sidebar,
|
||||
SidebarCollapseButton,
|
||||
} from "../sidebar";
|
||||
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
||||
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
|
||||
interface TabDataSource {
|
||||
items: InboxItem[];
|
||||
|
|
@ -130,22 +166,28 @@ function MainContentPanel({
|
|||
isChatPage,
|
||||
onTabSwitch,
|
||||
onNewChat,
|
||||
showRightPanelExpandButton = true,
|
||||
showTopBorder = false,
|
||||
children,
|
||||
}: {
|
||||
isChatPage: boolean;
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onNewChat?: () => void;
|
||||
showRightPanelExpandButton?: boolean;
|
||||
showTopBorder?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const activeTab = useAtomValue(activeTabAtom);
|
||||
const isDocumentTab = activeTab?.type === "document";
|
||||
|
||||
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
|
||||
onTabSwitch={onTabSwitch}
|
||||
onNewChat={onNewChat}
|
||||
rightActions={<RightPanelExpandButton />}
|
||||
rightActions={showRightPanelExpandButton ? <RightPanelExpandButton /> : null}
|
||||
className="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,
|
||||
}: LayoutShellProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const electronAPI = useElectronAPI();
|
||||
const isMacDesktop = electronAPI?.versions.platform === "darwin";
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed);
|
||||
const {
|
||||
|
|
@ -388,175 +432,190 @@ export function LayoutShell({
|
|||
return (
|
||||
<SidebarProvider value={sidebarContextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-screen w-full gap-2 px-2 py-0 overflow-hidden bg-rail",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<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 className={cn("flex h-screen w-full flex-col overflow-hidden bg-rail", className)}>
|
||||
{isMacDesktop ? (
|
||||
<MacDesktopTitleBar
|
||||
isSidebarCollapsed={isCollapsed}
|
||||
onToggleSidebar={toggleCollapsed}
|
||||
disableRightPanelToggle={useWorkspacePanel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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="relative hidden md:flex shrink-0 z-20 -mr-2 border-r bg-panel">
|
||||
<Sidebar
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={toggleCollapsed}
|
||||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
chats={chats}
|
||||
sharedChats={sharedChats}
|
||||
activeChatId={activeChatId}
|
||||
onNewChat={onNewChat}
|
||||
onChatSelect={onChatSelect}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
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"
|
||||
)}
|
||||
) : null}
|
||||
<div className="flex min-h-0 flex-1 w-full gap-2 px-2 py-0 overflow-hidden">
|
||||
<div className="hidden md:flex overflow-hidden -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>
|
||||
|
||||
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
||||
<SidebarSlideOutPanel
|
||||
open={anySlideOutOpen}
|
||||
onOpenChange={closeSlideout}
|
||||
ariaLabel={panelAriaLabel}
|
||||
{/* 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={cn(
|
||||
"relative hidden md:flex shrink-0 z-20 -mr-2 bg-panel",
|
||||
isMacDesktop ? "rounded-tl-xl border-t border-r border-l" : "border-r"
|
||||
)}
|
||||
>
|
||||
<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 === "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>
|
||||
<Sidebar
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={toggleCollapsed}
|
||||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
chats={chats}
|
||||
sharedChats={sharedChats}
|
||||
activeChatId={activeChatId}
|
||||
onNewChat={onNewChat}
|
||||
onChatSelect={onChatSelect}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
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}
|
||||
renderCollapseButton={!isMacDesktop}
|
||||
className={cn("flex shrink-0", isMacDesktop && "rounded-tl-xl")}
|
||||
isLoadingChats={isLoadingChats}
|
||||
sidebarWidth={sidebarWidth}
|
||||
isResizing={isResizing}
|
||||
/>
|
||||
|
||||
<DesktopWorkspaceRegion>
|
||||
{useWorkspacePanel ? (
|
||||
<WorkspacePanel
|
||||
viewportClassName={workspacePanelViewportClassName}
|
||||
contentClassName={workspacePanelContentClassName}
|
||||
{!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"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
||||
<SidebarSlideOutPanel
|
||||
open={anySlideOutOpen}
|
||||
onOpenChange={closeSlideout}
|
||||
ariaLabel={panelAriaLabel}
|
||||
>
|
||||
{children}
|
||||
</WorkspacePanel>
|
||||
) : (
|
||||
<>
|
||||
{/* Main content panel */}
|
||||
<MainContentPanel
|
||||
isChatPage={isChatPage}
|
||||
onTabSwitch={onTabSwitch}
|
||||
onNewChat={onNewChat}
|
||||
<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 === "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}
|
||||
</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) */}
|
||||
{documentsPanel ? (
|
||||
<RightPanel
|
||||
documentsPanel={{
|
||||
open: documentsPanel.open,
|
||||
onOpenChange: documentsPanel.onOpenChange,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</DesktopWorkspaceRegion>
|
||||
{/* Right panel — tabbed Sources/Report (desktop only) */}
|
||||
{documentsPanel ? (
|
||||
<RightPanel
|
||||
documentsPanel={{
|
||||
open: documentsPanel.open,
|
||||
onOpenChange: documentsPanel.onOpenChange,
|
||||
}}
|
||||
showCollapseButton={!isMacDesktop}
|
||||
showTopBorder={isMacDesktop}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</DesktopWorkspaceRegion>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarProvider>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue