mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55: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";
|
||||
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus, SquarePen } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
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 { SidebarUserProfile } from "../sidebar/SidebarUserProfile";
|
||||
import { SearchSpaceAvatar } from "./SearchSpaceAvatar";
|
||||
|
|
@ -17,6 +17,10 @@ interface IconRailProps {
|
|||
onSearchSpaceDelete?: (searchSpace: SearchSpace) => void;
|
||||
onSearchSpaceSettings?: (searchSpace: SearchSpace) => void;
|
||||
onAddSearchSpace: () => void;
|
||||
isSingleRailMode?: boolean;
|
||||
onNewChat?: () => void;
|
||||
navItems?: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
user: User;
|
||||
onUserSettings?: () => void;
|
||||
onLogout?: () => void;
|
||||
|
|
@ -32,6 +36,10 @@ export function IconRail({
|
|||
onSearchSpaceDelete,
|
||||
onSearchSpaceSettings,
|
||||
onAddSearchSpace,
|
||||
isSingleRailMode = false,
|
||||
onNewChat,
|
||||
navItems = [],
|
||||
onNavItemClick,
|
||||
user,
|
||||
onUserSettings,
|
||||
onLogout,
|
||||
|
|
@ -39,6 +47,29 @@ export function IconRail({
|
|||
setTheme,
|
||||
className,
|
||||
}: 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 (
|
||||
<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">
|
||||
|
|
@ -75,6 +106,33 @@ export function IconRail({
|
|||
Add search space
|
||||
</TooltipContent>
|
||||
</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>
|
||||
</ScrollArea>
|
||||
<SidebarUserProfile
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
MobileSidebarTrigger,
|
||||
Sidebar,
|
||||
} from "../sidebar";
|
||||
import { SidebarCollapseButton } from "../sidebar/SidebarCollapseButton";
|
||||
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
|
||||
import { TabBar } from "../tabs/TabBar";
|
||||
|
||||
|
|
@ -121,11 +122,13 @@ function MainContentPanel({
|
|||
isChatPage,
|
||||
onTabSwitch,
|
||||
onNewChat,
|
||||
leftActions,
|
||||
children,
|
||||
}: {
|
||||
isChatPage: boolean;
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onNewChat?: () => void;
|
||||
leftActions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const activeTab = useAtomValue(activeTabAtom);
|
||||
|
|
@ -136,6 +139,7 @@ function MainContentPanel({
|
|||
<TabBar
|
||||
onTabSwitch={onTabSwitch}
|
||||
onNewChat={onNewChat}
|
||||
leftActions={leftActions}
|
||||
rightActions={<RightPanelExpandButton />}
|
||||
className="min-w-0"
|
||||
/>
|
||||
|
|
@ -377,9 +381,9 @@ export function LayoutShell({
|
|||
<SidebarProvider value={sidebarContextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<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
|
||||
searchSpaces={searchSpaces}
|
||||
activeSearchSpaceId={activeSearchSpaceId}
|
||||
|
|
@ -387,6 +391,10 @@ export function LayoutShell({
|
|||
onSearchSpaceDelete={onSearchSpaceDelete}
|
||||
onSearchSpaceSettings={onSearchSpaceSettings}
|
||||
onAddSearchSpace={onAddSearchSpace}
|
||||
isSingleRailMode={isCollapsed}
|
||||
onNewChat={onNewChat}
|
||||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
user={user}
|
||||
onUserSettings={onUserSettings}
|
||||
onLogout={onLogout}
|
||||
|
|
@ -398,45 +406,44 @@ export function LayoutShell({
|
|||
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative hidden md:flex shrink-0 border bg-sidebar z-20 transition-[border-radius,border-color] duration-200",
|
||||
anySlideOutOpen ? "rounded-l-xl border-r-0 delay-0" : "rounded-xl delay-150"
|
||||
"relative hidden md:flex shrink-0 z-20",
|
||||
!isCollapsed && "bg-sidebar"
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
onLogout={onLogout}
|
||||
pageUsage={pageUsage}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
renderUserProfile={false}
|
||||
className={cn(
|
||||
"flex shrink-0 transition-[border-radius] duration-200",
|
||||
anySlideOutOpen ? "rounded-l-xl delay-0" : "rounded-xl delay-150"
|
||||
)}
|
||||
isLoadingChats={isLoadingChats}
|
||||
sidebarWidth={sidebarWidth}
|
||||
isResizing={isResizing}
|
||||
/>
|
||||
{!isCollapsed && (
|
||||
<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}
|
||||
onLogout={onLogout}
|
||||
pageUsage={pageUsage}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
renderUserProfile={false}
|
||||
className="flex shrink-0"
|
||||
isLoadingChats={isLoadingChats}
|
||||
sidebarWidth={sidebarWidth}
|
||||
isResizing={isResizing}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Unified slide-out panel — shell stays open, content cross-fades */}
|
||||
<SidebarSlideOutPanel
|
||||
|
|
@ -524,7 +531,16 @@ export function LayoutShell({
|
|||
)}
|
||||
|
||||
{/* 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}
|
||||
</MainContentPanel>
|
||||
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export function Sidebar({
|
|||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto border-t">
|
||||
<div className="mt-auto border-t border-border/60">
|
||||
{/* Platform navigation */}
|
||||
{navItems.length > 0 && (
|
||||
<NavSection items={navItems} onItemClick={onNavItemClick} isCollapsed={isCollapsed} />
|
||||
|
|
@ -307,7 +307,7 @@ function SidebarUsageFooter({
|
|||
|
||||
if (isAnonymous) {
|
||||
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 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
|
|
@ -340,7 +340,7 @@ function SidebarUsageFooter({
|
|||
}
|
||||
|
||||
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 />
|
||||
{pageUsage && (
|
||||
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ interface SidebarButtonProps {
|
|||
badge?: React.ReactNode;
|
||||
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
|
||||
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 */
|
||||
expandedIconNode?: React.ReactNode;
|
||||
/** Optional inline trailing content shown in expanded mode */
|
||||
|
|
@ -26,7 +28,7 @@ interface SidebarButtonProps {
|
|||
}
|
||||
|
||||
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",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
);
|
||||
|
|
@ -45,6 +47,7 @@ export function SidebarButton({
|
|||
isActive = false,
|
||||
badge,
|
||||
collapsedOverlay,
|
||||
collapsedIconNode,
|
||||
expandedIconNode,
|
||||
trailingContent,
|
||||
tooltipContent,
|
||||
|
|
@ -63,7 +66,7 @@ export function SidebarButton({
|
|||
className={cn(collapsedClassName, isActive && activeClassName, className)}
|
||||
{...buttonProps}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{collapsedIconNode ?? <Icon className="h-3.5 w-3.5" />}
|
||||
{collapsedOverlay}
|
||||
<span className="sr-only">{label}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function SidebarCollapseButton({
|
|||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent side={isCollapsed ? "right" : "bottom"}>
|
||||
<TooltipContent side="bottom" avoidCollisions={false}>
|
||||
<span className="flex items-center">
|
||||
{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}
|
||||
<ShortcutKbd keys={shortcutKeys("Mod", "\\")} />
|
||||
|
|
|
|||
|
|
@ -15,11 +15,18 @@ import { cn } from "@/lib/utils";
|
|||
interface TabBarProps {
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onNewChat?: () => void;
|
||||
leftActions?: React.ReactNode;
|
||||
rightActions?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TabBar({ onTabSwitch, onNewChat, rightActions, className }: TabBarProps) {
|
||||
export function TabBar({
|
||||
onTabSwitch,
|
||||
onNewChat,
|
||||
leftActions,
|
||||
rightActions,
|
||||
className,
|
||||
}: TabBarProps) {
|
||||
const tabs = useAtomValue(tabsAtom);
|
||||
const activeTabId = useAtomValue(activeTabIdAtom);
|
||||
const switchTab = useSetAtom(switchTabAtom);
|
||||
|
|
@ -68,11 +75,20 @@ export function TabBar({ onTabSwitch, onNewChat, rightActions, className }: TabB
|
|||
}
|
||||
}, [activeTabId]);
|
||||
|
||||
// Only show tab bar when there's more than one tab
|
||||
if (tabs.length <= 1) return null;
|
||||
// Keep action slots visible even with one/no tabs
|
||||
const hasAuxActions = !!leftActions || !!rightActions || !!onNewChat;
|
||||
const hasMultipleChats = tabs.length > 1;
|
||||
if (tabs.length <= 1 && !hasAuxActions) return null;
|
||||
|
||||
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
|
||||
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue