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

View file

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

View file

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

View file

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

View file

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

View file

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