From 360e21eee4ec4ab0458f82ce813d583a6d059a62 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 28 Apr 2026 23:58:00 +0530
Subject: [PATCH] feat(icon-rail, layout): enhance IconRail with new chat
functionality and navigation items; update LayoutShell to support collapsible
sidebar and integrate new actions
---
.../layout/ui/icon-rail/IconRail.tsx | 62 +++++++++++-
.../layout/ui/shell/LayoutShell.tsx | 96 +++++++++++--------
.../components/layout/ui/sidebar/Sidebar.tsx | 6 +-
.../layout/ui/sidebar/SidebarButton.tsx | 7 +-
.../ui/sidebar/SidebarCollapseButton.tsx | 2 +-
.../components/layout/ui/tabs/TabBar.tsx | 24 ++++-
6 files changed, 145 insertions(+), 52 deletions(-)
diff --git a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
index 756d6ffaf..c4b127c63 100644
--- a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
+++ b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
@@ -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 (
@@ -75,6 +106,33 @@ export function IconRail({
Add search space
+
+ {actionItems.length > 0 && (
+ <>
+
+ {actionItems.map(({ key, label, onClick, icon: Icon, isActive }) => (
+
+
+
+
+
+ {label}
+
+
+ ))}
+ >
+ )}
void;
onNewChat?: () => void;
+ leftActions?: React.ReactNode;
children: React.ReactNode;
}) {
const activeTab = useAtomValue(activeTabAtom);
@@ -136,6 +139,7 @@ function MainContentPanel({
}
className="min-w-0"
/>
@@ -377,9 +381,9 @@ export function LayoutShell({
-
+
-
+ {!isCollapsed && (
+
+ )}
{/* Unified slide-out panel — shell stays open, content cross-fades */}
+
+ ) : undefined
+ }
+ >
{children}
diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
index f0deea038..00263e81a 100644
--- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
@@ -269,7 +269,7 @@ export function Sidebar({
)}
{/* Footer */}
-
+
{/* Platform navigation */}
{navItems.length > 0 && (
@@ -307,7 +307,7 @@ function SidebarUsageFooter({
if (isAnonymous) {
return (
-
+
{pageUsage && (
@@ -340,7 +340,7 @@ function SidebarUsageFooter({
}
return (
-
+
{pageUsage && (
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
index 9add894db..a3c3b383c 100644
--- a/surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/SidebarButton.tsx
@@ -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
rendering */
+ collapsedIconNode?: React.ReactNode;
/** Custom icon node for expanded mode — overrides the default 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}
>
-
+ {collapsedIconNode ?? }
{collapsedOverlay}
{label}
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx
index 0eb409349..06f1479cf 100644
--- a/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx
@@ -35,7 +35,7 @@ export function SidebarCollapseButton({
return (
{button}
-
+
{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}
diff --git a/surfsense_web/components/layout/ui/tabs/TabBar.tsx b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
index 8d0d986d3..a246ff849 100644
--- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx
+++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
@@ -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 (
-
+
+ {leftActions ?
{leftActions}
: null}