mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
feat(icon-rail, layout): enhance IconRail with new chat functionality and navigation items; update LayoutShell to support collapsible sidebar and integrate new actions
This commit is contained in:
parent
a869069a0d
commit
360e21eee4
6 changed files with 145 additions and 52 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Plus } from "lucide-react";
|
import { Plus, SquarePen } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { SearchSpace } from "../../types/layout.types";
|
import type { NavItem, SearchSpace } from "../../types/layout.types";
|
||||||
import type { User } from "../../types/layout.types";
|
import type { User } from "../../types/layout.types";
|
||||||
import { SidebarUserProfile } from "../sidebar/SidebarUserProfile";
|
import { SidebarUserProfile } from "../sidebar/SidebarUserProfile";
|
||||||
import { SearchSpaceAvatar } from "./SearchSpaceAvatar";
|
import { SearchSpaceAvatar } from "./SearchSpaceAvatar";
|
||||||
|
|
@ -17,6 +17,10 @@ interface IconRailProps {
|
||||||
onSearchSpaceDelete?: (searchSpace: SearchSpace) => void;
|
onSearchSpaceDelete?: (searchSpace: SearchSpace) => void;
|
||||||
onSearchSpaceSettings?: (searchSpace: SearchSpace) => void;
|
onSearchSpaceSettings?: (searchSpace: SearchSpace) => void;
|
||||||
onAddSearchSpace: () => void;
|
onAddSearchSpace: () => void;
|
||||||
|
isSingleRailMode?: boolean;
|
||||||
|
onNewChat?: () => void;
|
||||||
|
navItems?: NavItem[];
|
||||||
|
onNavItemClick?: (item: NavItem) => void;
|
||||||
user: User;
|
user: User;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
|
|
@ -32,6 +36,10 @@ export function IconRail({
|
||||||
onSearchSpaceDelete,
|
onSearchSpaceDelete,
|
||||||
onSearchSpaceSettings,
|
onSearchSpaceSettings,
|
||||||
onAddSearchSpace,
|
onAddSearchSpace,
|
||||||
|
isSingleRailMode = false,
|
||||||
|
onNewChat,
|
||||||
|
navItems = [],
|
||||||
|
onNavItemClick,
|
||||||
user,
|
user,
|
||||||
onUserSettings,
|
onUserSettings,
|
||||||
onLogout,
|
onLogout,
|
||||||
|
|
@ -39,6 +47,29 @@ export function IconRail({
|
||||||
setTheme,
|
setTheme,
|
||||||
className,
|
className,
|
||||||
}: IconRailProps) {
|
}: IconRailProps) {
|
||||||
|
const actionItems = isSingleRailMode
|
||||||
|
? [
|
||||||
|
...(onNewChat
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "new-chat",
|
||||||
|
label: "New chat",
|
||||||
|
onClick: onNewChat,
|
||||||
|
icon: SquarePen,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...navItems.map((item) => ({
|
||||||
|
key: item.url,
|
||||||
|
label: item.title,
|
||||||
|
onClick: () => onNavItemClick?.(item),
|
||||||
|
icon: item.icon,
|
||||||
|
isActive: !!item.isActive,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex h-full w-14 min-h-0 flex-col items-center", className)}>
|
<div className={cn("flex h-full w-14 min-h-0 flex-col items-center", className)}>
|
||||||
<ScrollArea className="w-full min-h-0 flex-1">
|
<ScrollArea className="w-full min-h-0 flex-1">
|
||||||
|
|
@ -75,6 +106,33 @@ export function IconRail({
|
||||||
Add search space
|
Add search space
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{actionItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="my-1 h-px w-8 bg-border/60" />
|
||||||
|
{actionItems.map(({ key, label, onClick, icon: Icon, isActive }) => (
|
||||||
|
<Tooltip key={key}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"h-10 w-10 rounded-lg",
|
||||||
|
isActive && "bg-accent text-accent-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
<span className="sr-only">{label}</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
{label}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<SidebarUserProfile
|
<SidebarUserProfile
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
MobileSidebarTrigger,
|
MobileSidebarTrigger,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
} from "../sidebar";
|
} from "../sidebar";
|
||||||
|
import { SidebarCollapseButton } from "../sidebar/SidebarCollapseButton";
|
||||||
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
||||||
import { TabBar } from "../tabs/TabBar";
|
import { TabBar } from "../tabs/TabBar";
|
||||||
|
|
||||||
|
|
@ -121,11 +122,13 @@ function MainContentPanel({
|
||||||
isChatPage,
|
isChatPage,
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
|
leftActions,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
isChatPage: boolean;
|
isChatPage: boolean;
|
||||||
onTabSwitch?: (tab: Tab) => void;
|
onTabSwitch?: (tab: Tab) => void;
|
||||||
onNewChat?: () => void;
|
onNewChat?: () => void;
|
||||||
|
leftActions?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const activeTab = useAtomValue(activeTabAtom);
|
const activeTab = useAtomValue(activeTabAtom);
|
||||||
|
|
@ -136,6 +139,7 @@ function MainContentPanel({
|
||||||
<TabBar
|
<TabBar
|
||||||
onTabSwitch={onTabSwitch}
|
onTabSwitch={onTabSwitch}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
|
leftActions={leftActions}
|
||||||
rightActions={<RightPanelExpandButton />}
|
rightActions={<RightPanelExpandButton />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|
@ -377,9 +381,9 @@ export function LayoutShell({
|
||||||
<SidebarProvider value={sidebarContextValue}>
|
<SidebarProvider value={sidebarContextValue}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div
|
<div
|
||||||
className={cn("flex h-screen w-full gap-2 p-2 overflow-hidden bg-muted/40", className)}
|
className={cn("flex h-screen w-full gap-2 px-2 py-0 overflow-hidden bg-muted/40", className)}
|
||||||
>
|
>
|
||||||
<div className="hidden md:flex overflow-hidden">
|
<div className="hidden md:flex overflow-hidden border-r border-border/60 -mr-2 pr-2">
|
||||||
<IconRail
|
<IconRail
|
||||||
searchSpaces={searchSpaces}
|
searchSpaces={searchSpaces}
|
||||||
activeSearchSpaceId={activeSearchSpaceId}
|
activeSearchSpaceId={activeSearchSpaceId}
|
||||||
|
|
@ -387,6 +391,10 @@ export function LayoutShell({
|
||||||
onSearchSpaceDelete={onSearchSpaceDelete}
|
onSearchSpaceDelete={onSearchSpaceDelete}
|
||||||
onSearchSpaceSettings={onSearchSpaceSettings}
|
onSearchSpaceSettings={onSearchSpaceSettings}
|
||||||
onAddSearchSpace={onAddSearchSpace}
|
onAddSearchSpace={onAddSearchSpace}
|
||||||
|
isSingleRailMode={isCollapsed}
|
||||||
|
onNewChat={onNewChat}
|
||||||
|
navItems={navItems}
|
||||||
|
onNavItemClick={onNavItemClick}
|
||||||
user={user}
|
user={user}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
|
|
@ -398,45 +406,44 @@ export function LayoutShell({
|
||||||
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content */}
|
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative hidden md:flex shrink-0 border bg-sidebar z-20 transition-[border-radius,border-color] duration-200",
|
"relative hidden md:flex shrink-0 z-20",
|
||||||
anySlideOutOpen ? "rounded-l-xl border-r-0 delay-0" : "rounded-xl delay-150"
|
!isCollapsed && "bg-sidebar"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Sidebar
|
{!isCollapsed && (
|
||||||
searchSpace={searchSpace}
|
<Sidebar
|
||||||
isCollapsed={isCollapsed}
|
searchSpace={searchSpace}
|
||||||
onToggleCollapse={toggleCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
navItems={navItems}
|
onToggleCollapse={toggleCollapsed}
|
||||||
onNavItemClick={onNavItemClick}
|
navItems={navItems}
|
||||||
chats={chats}
|
onNavItemClick={onNavItemClick}
|
||||||
sharedChats={sharedChats}
|
chats={chats}
|
||||||
activeChatId={activeChatId}
|
sharedChats={sharedChats}
|
||||||
onNewChat={onNewChat}
|
activeChatId={activeChatId}
|
||||||
onChatSelect={onChatSelect}
|
onNewChat={onNewChat}
|
||||||
onChatRename={onChatRename}
|
onChatSelect={onChatSelect}
|
||||||
onChatDelete={onChatDelete}
|
onChatRename={onChatRename}
|
||||||
onChatArchive={onChatArchive}
|
onChatDelete={onChatDelete}
|
||||||
onViewAllSharedChats={onViewAllSharedChats}
|
onChatArchive={onChatArchive}
|
||||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
onViewAllSharedChats={onViewAllSharedChats}
|
||||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
||||||
user={user}
|
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
||||||
onSettings={onSettings}
|
user={user}
|
||||||
onManageMembers={onManageMembers}
|
onSettings={onSettings}
|
||||||
onUserSettings={onUserSettings}
|
onManageMembers={onManageMembers}
|
||||||
onLogout={onLogout}
|
onUserSettings={onUserSettings}
|
||||||
pageUsage={pageUsage}
|
onLogout={onLogout}
|
||||||
theme={theme}
|
pageUsage={pageUsage}
|
||||||
setTheme={setTheme}
|
theme={theme}
|
||||||
renderUserProfile={false}
|
setTheme={setTheme}
|
||||||
className={cn(
|
renderUserProfile={false}
|
||||||
"flex shrink-0 transition-[border-radius] duration-200",
|
className="flex shrink-0"
|
||||||
anySlideOutOpen ? "rounded-l-xl delay-0" : "rounded-xl delay-150"
|
isLoadingChats={isLoadingChats}
|
||||||
)}
|
sidebarWidth={sidebarWidth}
|
||||||
isLoadingChats={isLoadingChats}
|
isResizing={isResizing}
|
||||||
sidebarWidth={sidebarWidth}
|
/>
|
||||||
isResizing={isResizing}
|
)}
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
||||||
<SidebarSlideOutPanel
|
<SidebarSlideOutPanel
|
||||||
|
|
@ -524,7 +531,16 @@ export function LayoutShell({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main content panel */}
|
{/* Main content panel */}
|
||||||
<MainContentPanel isChatPage={isChatPage} onTabSwitch={onTabSwitch} onNewChat={onNewChat}>
|
<MainContentPanel
|
||||||
|
isChatPage={isChatPage}
|
||||||
|
onTabSwitch={onTabSwitch}
|
||||||
|
onNewChat={onNewChat}
|
||||||
|
leftActions={
|
||||||
|
isCollapsed ? (
|
||||||
|
<SidebarCollapseButton isCollapsed={isCollapsed} onToggle={toggleCollapsed} />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</MainContentPanel>
|
</MainContentPanel>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ export function Sidebar({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="mt-auto border-t">
|
<div className="mt-auto border-t border-border/60">
|
||||||
{/* Platform navigation */}
|
{/* Platform navigation */}
|
||||||
{navItems.length > 0 && (
|
{navItems.length > 0 && (
|
||||||
<NavSection items={navItems} onItemClick={onNavItemClick} isCollapsed={isCollapsed} />
|
<NavSection items={navItems} onItemClick={onNavItemClick} isCollapsed={isCollapsed} />
|
||||||
|
|
@ -307,7 +307,7 @@ function SidebarUsageFooter({
|
||||||
|
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
return (
|
return (
|
||||||
<div className="px-3 py-3 border-t space-y-3">
|
<div className="px-3 py-3 border-t border-border/60 space-y-3">
|
||||||
{pageUsage && (
|
{pageUsage && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex justify-between items-center text-xs">
|
<div className="flex justify-between items-center text-xs">
|
||||||
|
|
@ -340,7 +340,7 @@ function SidebarUsageFooter({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3 py-3 border-t space-y-3">
|
<div className="px-3 py-3 border-t border-border/60 space-y-3">
|
||||||
<PremiumTokenUsageDisplay />
|
<PremiumTokenUsageDisplay />
|
||||||
{pageUsage && (
|
{pageUsage && (
|
||||||
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ interface SidebarButtonProps {
|
||||||
badge?: React.ReactNode;
|
badge?: React.ReactNode;
|
||||||
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
|
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
|
||||||
collapsedOverlay?: React.ReactNode;
|
collapsedOverlay?: React.ReactNode;
|
||||||
|
/** Custom icon node for collapsed mode — overrides the default <Icon> rendering */
|
||||||
|
collapsedIconNode?: React.ReactNode;
|
||||||
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
|
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
|
||||||
expandedIconNode?: React.ReactNode;
|
expandedIconNode?: React.ReactNode;
|
||||||
/** Optional inline trailing content shown in expanded mode */
|
/** Optional inline trailing content shown in expanded mode */
|
||||||
|
|
@ -26,7 +28,7 @@ interface SidebarButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const expandedClassName = cn(
|
const expandedClassName = cn(
|
||||||
"flex items-center gap-1.5 rounded-md mx-2 px-2 py-1 text-sm transition-colors text-left",
|
"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",
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
);
|
);
|
||||||
|
|
@ -45,6 +47,7 @@ export function SidebarButton({
|
||||||
isActive = false,
|
isActive = false,
|
||||||
badge,
|
badge,
|
||||||
collapsedOverlay,
|
collapsedOverlay,
|
||||||
|
collapsedIconNode,
|
||||||
expandedIconNode,
|
expandedIconNode,
|
||||||
trailingContent,
|
trailingContent,
|
||||||
tooltipContent,
|
tooltipContent,
|
||||||
|
|
@ -63,7 +66,7 @@ export function SidebarButton({
|
||||||
className={cn(collapsedClassName, isActive && activeClassName, className)}
|
className={cn(collapsedClassName, isActive && activeClassName, className)}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
<Icon className="h-3.5 w-3.5" />
|
{collapsedIconNode ?? <Icon className="h-3.5 w-3.5" />}
|
||||||
{collapsedOverlay}
|
{collapsedOverlay}
|
||||||
<span className="sr-only">{label}</span>
|
<span className="sr-only">{label}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export function SidebarCollapseButton({
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
<TooltipContent side={isCollapsed ? "right" : "bottom"}>
|
<TooltipContent side="bottom" avoidCollisions={false}>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}
|
{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}
|
||||||
<ShortcutKbd keys={shortcutKeys("Mod", "\\")} />
|
<ShortcutKbd keys={shortcutKeys("Mod", "\\")} />
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,18 @@ import { cn } from "@/lib/utils";
|
||||||
interface TabBarProps {
|
interface TabBarProps {
|
||||||
onTabSwitch?: (tab: Tab) => void;
|
onTabSwitch?: (tab: Tab) => void;
|
||||||
onNewChat?: () => void;
|
onNewChat?: () => void;
|
||||||
|
leftActions?: React.ReactNode;
|
||||||
rightActions?: React.ReactNode;
|
rightActions?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabBar({ onTabSwitch, onNewChat, rightActions, className }: TabBarProps) {
|
export function TabBar({
|
||||||
|
onTabSwitch,
|
||||||
|
onNewChat,
|
||||||
|
leftActions,
|
||||||
|
rightActions,
|
||||||
|
className,
|
||||||
|
}: TabBarProps) {
|
||||||
const tabs = useAtomValue(tabsAtom);
|
const tabs = useAtomValue(tabsAtom);
|
||||||
const activeTabId = useAtomValue(activeTabIdAtom);
|
const activeTabId = useAtomValue(activeTabIdAtom);
|
||||||
const switchTab = useSetAtom(switchTabAtom);
|
const switchTab = useSetAtom(switchTabAtom);
|
||||||
|
|
@ -68,11 +75,20 @@ export function TabBar({ onTabSwitch, onNewChat, rightActions, className }: TabB
|
||||||
}
|
}
|
||||||
}, [activeTabId]);
|
}, [activeTabId]);
|
||||||
|
|
||||||
// Only show tab bar when there's more than one tab
|
// Keep action slots visible even with one/no tabs
|
||||||
if (tabs.length <= 1) return null;
|
const hasAuxActions = !!leftActions || !!rightActions || !!onNewChat;
|
||||||
|
const hasMultipleChats = tabs.length > 1;
|
||||||
|
if (tabs.length <= 1 && !hasAuxActions) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mb-2 flex h-9 items-center shrink-0 px-1 gap-0.5 select-none", className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-2 flex h-9 items-center shrink-0 px-1 gap-0.5 select-none",
|
||||||
|
hasMultipleChats && "mt-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{leftActions ? <div className="flex items-center gap-0.5 shrink-0">{leftActions}</div> : null}
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="flex h-full items-center flex-1 gap-0.5 overflow-x-auto overflow-y-hidden scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden py-1"
|
className="flex h-full items-center flex-1 gap-0.5 overflow-x-auto overflow-y-hidden scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden py-1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue