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:
Anish Sarkar 2026-04-28 23:58:00 +05:30
parent a869069a0d
commit 360e21eee4
6 changed files with 145 additions and 52 deletions

View file

@ -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

View file

@ -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>

View file

@ -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} />

View file

@ -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>

View file

@ -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", "\\")} />

View file

@ -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"