+
{mentionedDocs && mentionedDocs.length > 0 ? (
) : (
diff --git a/surfsense_web/components/layout/ui/tabs/TabBar.tsx b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
index 2d013eaaa..3015fcf51 100644
--- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx
+++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
@@ -89,7 +89,7 @@ export function TabBar({
{leftActions ?
{leftActions}
: null}
{tabs.map((tab) => {
const isActive = tab.id === activeTabId;
diff --git a/surfsense_web/components/public-chat/public-thread.tsx b/surfsense_web/components/public-chat/public-thread.tsx
index 627baf831..19a13ab54 100644
--- a/surfsense_web/components/public-chat/public-thread.tsx
+++ b/surfsense_web/components/public-chat/public-thread.tsx
@@ -120,7 +120,7 @@ const PublicUserMessage: FC = () => {
>
-
From 481397469045c555eb92a975b932f9e22b104e7f Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Wed, 29 Apr 2026 14:21:29 +0530
Subject: [PATCH 07/76] fix(ui): update hover styles for buttons in RightPanel,
Sidebar, and TabBar components
---
surfsense_web/components/layout/ui/right-panel/RightPanel.tsx | 4 ++--
.../components/layout/ui/sidebar/SidebarCollapseButton.tsx | 2 +-
surfsense_web/components/layout/ui/tabs/TabBar.tsx | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
index 88ca66954..3dc7a96d4 100644
--- a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
+++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
@@ -61,7 +61,7 @@ function CollapseButton({ onClick }: { onClick: () => void }) {
variant="ghost"
size="icon"
onClick={onClick}
- className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted/40"
+ className="h-8 w-8 shrink-0 text-muted-foreground hover:text-muted-foreground hover:bg-muted/40"
>
Collapse panel
@@ -102,7 +102,7 @@ export function RightPanelExpandButton() {
variant="ghost"
size="icon"
onClick={() => startTransition(() => setCollapsed(false))}
- className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/40"
+ className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:text-muted-foreground hover:bg-muted/40"
>
Expand panel
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx
index 7f8844b8d..7228c1fd9 100644
--- a/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/SidebarCollapseButton.tsx
@@ -26,7 +26,7 @@ export function SidebarCollapseButton({
variant="ghost"
size="icon"
onClick={onToggle}
- className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted/40"
+ className="h-8 w-8 shrink-0 text-muted-foreground hover:text-muted-foreground hover:bg-muted/40"
>
{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 3015fcf51..c6f892034 100644
--- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx
+++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
@@ -139,7 +139,7 @@ export function TabBar({
- {/* 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. Negative right margin closes the flex gap so the sidebar sits flush against the main panel, separated only by a border. */}
@@ -445,6 +445,16 @@ export function LayoutShell({
/>
)}
+ {/* Invisible drag hit-area straddling the right border — provides resize affordance without any visible UI */}
+ {!isCollapsed && (
+
+ )}
+
{/* Unified slide-out panel — shell stays open, content cross-fades */}
- {/* Resize handle — negative margins eat the flex gap so spacing stays unchanged */}
- {!isCollapsed && (
-
- )}
-
{/* Main content panel */}
Date: Sun, 3 May 2026 18:36:23 +0530
Subject: [PATCH 09/76] feat(tabs): persist tabs in localStorage for improved
user experience across sessions; enhance syncChatTabAtom to include
searchSpaceId; update LayoutDataProvider to support announcements dialog and
clean up unused imports
---
surfsense_web/atoms/tabs/tabs.atom.ts | 39 +++++--
.../layout/providers/LayoutDataProvider.tsx | 56 ++++------
.../components/layout/ui/tabs/TabBar.tsx | 101 +++++++++++++++---
3 files changed, 139 insertions(+), 57 deletions(-)
diff --git a/surfsense_web/atoms/tabs/tabs.atom.ts b/surfsense_web/atoms/tabs/tabs.atom.ts
index 22cc5373a..3bb06c8e3 100644
--- a/surfsense_web/atoms/tabs/tabs.atom.ts
+++ b/surfsense_web/atoms/tabs/tabs.atom.ts
@@ -36,14 +36,16 @@ const initialState: TabsState = {
// Prevent race conditions where route-sync recreates a just-deleted chat tab.
const deletedChatIdsAtom = atom>(new Set());
-const sessionStorageAdapter = createJSONStorage(
- () => (typeof window !== "undefined" ? sessionStorage : undefined) as Storage
+// Persist tabs in localStorage so they survive a hard refresh and let the user
+// keep tabs open across multiple search spaces (browser-like behavior).
+const localStorageAdapter = createJSONStorage(
+ () => (typeof window !== "undefined" ? localStorage : undefined) as Storage
);
export const tabsStateAtom = atomWithStorage(
"surfsense:tabs",
initialState,
- sessionStorageAdapter,
+ localStorageAdapter,
{ getOnInit: true }
);
@@ -72,7 +74,17 @@ export const syncChatTabAtom = atom(
(
get,
set,
- { chatId, title, chatUrl }: { chatId: number | null; title?: string; chatUrl?: string }
+ {
+ chatId,
+ title,
+ chatUrl,
+ searchSpaceId,
+ }: {
+ chatId: number | null;
+ title?: string;
+ chatUrl?: string;
+ searchSpaceId: number;
+ }
) => {
if (chatId && get(deletedChatIdsAtom).has(chatId)) {
return;
@@ -87,20 +99,32 @@ export const syncChatTabAtom = atom(
...state,
activeTabId: tabId,
tabs: state.tabs.map((t) =>
- t.id === tabId ? { ...t, title: title || t.title, chatUrl: chatUrl || t.chatUrl } : t
+ t.id === tabId
+ ? {
+ ...t,
+ title: title || t.title,
+ chatUrl: chatUrl || t.chatUrl,
+ searchSpaceId: searchSpaceId ?? t.searchSpaceId,
+ }
+ : t
),
});
return;
}
// If navigating to a new chat (no chatId), ensure there's a "new chat" tab
+ // scoped to the current search space.
if (!chatId) {
const hasNewChatTab = state.tabs.some((t) => t.id === "chat-new");
if (hasNewChatTab) {
- set(tabsStateAtom, { ...state, activeTabId: "chat-new" });
+ set(tabsStateAtom, {
+ ...state,
+ activeTabId: "chat-new",
+ tabs: state.tabs.map((t) => (t.id === "chat-new" ? { ...t, searchSpaceId, chatUrl } : t)),
+ });
} else {
set(tabsStateAtom, {
- tabs: [...state.tabs, INITIAL_CHAT_TAB],
+ tabs: [...state.tabs, { ...INITIAL_CHAT_TAB, searchSpaceId, chatUrl }],
activeTabId: "chat-new",
});
}
@@ -115,6 +139,7 @@ export const syncChatTabAtom = atom(
title: title || `Chat ${chatId}`,
chatId,
chatUrl,
+ searchSpaceId,
};
let updatedTabs: Tab[];
diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
index d70a7ade4..c125ff0eb 100644
--- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
@@ -2,7 +2,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
-import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
+import { AlertTriangle, Inbox, SquareLibrary } from "lucide-react";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
@@ -15,18 +15,15 @@ import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import {
+ announcementsDialogAtom,
searchSpaceSettingsDialogAtom,
teamDialogAtom,
userSettingsDialogAtom,
} from "@/atoms/settings/settings-dialog.atoms";
-import {
- removeChatTabAtom,
- resetTabsAtom,
- syncChatTabAtom,
- type Tab,
-} from "@/atoms/tabs/tabs.atom";
+import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { ActionLogSheet } from "@/components/agent-action-log/action-log-sheet";
+import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog";
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
import { TeamDialog } from "@/components/settings/team-dialog";
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
@@ -105,7 +102,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const currentThreadState = useAtomValue(currentThreadAtom);
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
const syncChatTab = useSetAtom(syncChatTabAtom);
- const resetTabs = useSetAtom(resetTabsAtom);
const removeChatTab = useSetAtom(removeChatTabAtom);
// Key used to force-remount the page component (e.g. after deleting the active chat
@@ -132,11 +128,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
});
// Unified slide-out panel state (only one can be open at a time)
- type SlideoutPanel = "inbox" | "shared" | "private" | "announcements" | null;
+ type SlideoutPanel = "inbox" | "shared" | "private" | null;
const [activeSlideoutPanel, setActiveSlideoutPanel] = useState(null);
const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
- const isAnnouncementsSidebarOpen = activeSlideoutPanel === "announcements";
// Documents sidebar state (shared atom so Composer can toggle it)
const [isDocumentsSidebarOpen, setIsDocumentsSidebarOpen] = useAtom(documentsSidebarOpenAtom);
@@ -252,16 +247,16 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const [isDeletingSearchSpace, setIsDeletingSearchSpace] = useState(false);
const [isLeavingSearchSpace, setIsLeavingSearchSpace] = useState(false);
- // Reset transient slide-out panels and tabs when switching search spaces.
- // Use a ref to skip the initial mount — only reset when the space actually changes.
+ // Reset transient slide-out panels when switching search spaces.
+ // Tabs intentionally persist across spaces — opening tabs from multiple
+ // search spaces is a supported flow (browser-tab semantics).
const prevSearchSpaceIdRef = useRef(searchSpaceId);
useEffect(() => {
if (prevSearchSpaceIdRef.current !== searchSpaceId) {
prevSearchSpaceIdRef.current = searchSpaceId;
setActiveSlideoutPanel(null);
- resetTabs();
}
- }, [searchSpaceId, resetTabs]);
+ }, [searchSpaceId]);
const searchSpaces: SearchSpace[] = useMemo(() => {
if (!searchSpacesData || !Array.isArray(searchSpacesData)) return [];
@@ -313,6 +308,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
// Avoid overwriting live SSE-updated tab titles with fallback values.
title: chatId ? (thread?.title ?? undefined) : "New Chat",
chatUrl,
+ searchSpaceId: Number(searchSpaceId),
});
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
@@ -347,6 +343,9 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}, [threadsData, searchSpaceId]);
// Navigation items
+ // Inbox is rendered explicitly below "New chat" in the sidebar (it is also
+ // surfaced in the icon rail's collapsed mode via this list). Announcements
+ // has been moved to the avatar dropdown and is no longer a nav item.
const navItems: NavItem[] = useMemo(
() =>
(
@@ -366,24 +365,9 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
isActive: isDocumentsSidebarOpen,
}
: null,
- {
- title: "Announcements",
- url: "#announcements",
- icon: Megaphone,
- isActive: isAnnouncementsSidebarOpen,
- badge:
- announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined,
- },
] as (NavItem | null)[]
).filter((item): item is NavItem => item !== null),
- [
- isMobile,
- isInboxSidebarOpen,
- isDocumentsSidebarOpen,
- totalUnreadCount,
- isAnnouncementsSidebarOpen,
- announcementUnreadCount,
- ]
+ [isMobile, isInboxSidebarOpen, isDocumentsSidebarOpen, totalUnreadCount]
);
// Handlers
@@ -401,11 +385,16 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom);
const setTeamDialogOpen = useSetAtom(teamDialogAtom);
+ const setAnnouncementsDialog = useSetAtom(announcementsDialogAtom);
const handleUserSettings = useCallback(() => {
setUserSettingsDialog({ open: true, initialTab: "profile" });
}, [setUserSettingsDialog]);
+ const handleAnnouncements = useCallback(() => {
+ setAnnouncementsDialog(true);
+ }, [setAnnouncementsDialog]);
+
const handleSearchSpaceSettings = useCallback(
(_space: SearchSpace) => {
setSearchSpaceSettingsDialog({ open: true, initialTab: "general" });
@@ -519,10 +508,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}
return;
}
- if (item.url === "#announcements") {
- setActiveSlideoutPanel((prev) => (prev === "announcements" ? null : "announcements"));
- return;
- }
router.push(item.url);
},
[router, isMobile, isDocumentsSidebarOpen, setIsDocumentsSidebarOpen, setIsRightPanelCollapsed]
@@ -714,6 +699,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
onSettings={handleSettings}
onManageMembers={handleManageMembers}
onUserSettings={handleUserSettings}
+ onAnnouncements={handleAnnouncements}
+ announcementUnreadCount={announcementUnreadCount}
onLogout={handleLogout}
theme={theme}
setTheme={setTheme}
@@ -901,6 +888,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
+
{/* Agent action log + revert sheet */}
diff --git a/surfsense_web/components/layout/ui/tabs/TabBar.tsx b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
index c6f892034..f3c88bf6a 100644
--- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx
+++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
@@ -20,6 +20,19 @@ interface TabBarProps {
className?: string;
}
+// Pure scroll-target calculation (port of opencode's nextTabListScrollLeft).
+// - When the list shrinks (a tab was closed), do not move the scroll.
+// - When the list overflows after growing, snap to the right edge so the new tab is visible.
+function nextTabListScrollLeft(input: {
+ prevScrollWidth: number;
+ scrollWidth: number;
+ clientWidth: number;
+}) {
+ if (input.scrollWidth <= input.prevScrollWidth) return;
+ if (input.scrollWidth <= input.clientWidth) return;
+ return input.scrollWidth - input.clientWidth;
+}
+
export function TabBar({
onTabSwitch,
onNewChat,
@@ -53,7 +66,56 @@ export function TabBar({
[closeTab, onTabSwitch]
);
- // Keep active tab visible with minimal scroll shift.
+ // React to tab list growth (port of opencode's createFileTabListSync).
+ // Uses a MutationObserver instead of a tab-id effect so the scroll catches
+ // the moment the new tab is added to the DOM, not after activation lands.
+ // Also remaps vertical wheel motion to horizontal scroll.
+ useEffect(() => {
+ const el = scrollRef.current;
+ if (!el) return;
+
+ let prevScrollWidth = el.scrollWidth;
+ let frame: number | undefined;
+
+ const update = () => {
+ const left = nextTabListScrollLeft({
+ prevScrollWidth,
+ scrollWidth: el.scrollWidth,
+ clientWidth: el.clientWidth,
+ });
+ if (left !== undefined) {
+ el.scrollTo({ left, behavior: "smooth" });
+ }
+ prevScrollWidth = el.scrollWidth;
+ };
+
+ const schedule = () => {
+ if (frame !== undefined) cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(() => {
+ frame = undefined;
+ update();
+ });
+ };
+
+ const onWheel = (e: WheelEvent) => {
+ if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
+ el.scrollLeft += e.deltaY > 0 ? 50 : -50;
+ e.preventDefault();
+ };
+
+ el.addEventListener("wheel", onWheel, { passive: false });
+ const observer = new MutationObserver(schedule);
+ observer.observe(el, { childList: true });
+
+ return () => {
+ el.removeEventListener("wheel", onWheel);
+ observer.disconnect();
+ if (frame !== undefined) cancelAnimationFrame(frame);
+ };
+ }, []);
+
+ // When the user activates a tab that's currently off-screen (e.g. clicked
+ // from the sidebar), nudge the scroller minimally so the active tab is in view.
useEffect(() => {
if (!scrollRef.current || !activeTabId) return;
const scroller = scrollRef.current;
@@ -75,10 +137,6 @@ export function TabBar({
}
}, [activeTabId]);
- // Keep action slots visible even with one/no tabs
- const hasAuxActions = !!leftActions || !!rightActions || !!onNewChat;
- if (tabs.length <= 1 && !hasAuxActions) return null;
-
return (
handleTabClick(tab)}
className={cn(
- "group relative flex h-full w-[150px] items-center px-3 min-h-0 overflow-hidden text-[13px] font-medium rounded-md transition-all duration-150 shrink-0",
+ "group relative flex h-full items-center px-3 min-w-[120px] max-w-[240px] min-h-0 overflow-hidden text-[13px] font-medium rounded-md transition-all duration-150 shrink-0",
isActive
? "bg-muted/60 text-foreground"
: "bg-transparent text-muted-foreground hover:bg-muted/30 hover:text-foreground"
@@ -133,20 +191,31 @@ export function TabBar({
);
})}
-
-
{onNewChat && (
-
+
+
)}
- {rightActions}
+ {rightActions ? (
+
{rightActions}
+ ) : null}
);
}
From a9192beae3e87e937e8fce37bb9158595410fd44 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 3 May 2026 18:42:29 +0530
Subject: [PATCH 10/76] feat(announcements): rename "Announcements" to "What's
New" across the application; add AnnouncementsDialog component for displaying
updates; update empty state messaging; remove unused AnnouncementsSidebar
component.
---
.../app/(home)/announcements/layout.tsx | 6 +-
.../app/(home)/announcements/page.tsx | 2 +-
.../atoms/settings/settings-dialog.atoms.ts | 2 +
.../announcements/AnnouncementsDialog.tsx | 50 +++++++++++
.../announcements/AnnouncementsEmptyState.tsx | 4 +-
.../components/homepage/footer-new.tsx | 2 +-
.../providers/FreeLayoutDataProvider.tsx | 46 +++++-----
.../layout/ui/icon-rail/IconRail.tsx | 6 ++
.../layout/ui/shell/LayoutShell.tsx | 44 +++-------
.../ui/sidebar/AnnouncementsSidebar.tsx | 84 -------------------
.../layout/ui/sidebar/MobileSidebar.tsx | 13 +++
.../layout/ui/sidebar/NavSection.tsx | 7 +-
.../components/layout/ui/sidebar/Sidebar.tsx | 41 ++++++++-
.../layout/ui/sidebar/SidebarUserProfile.tsx | 37 ++++++++
.../components/layout/ui/sidebar/index.ts | 1 -
.../lib/announcements/announcements-data.ts | 4 +-
16 files changed, 189 insertions(+), 160 deletions(-)
create mode 100644 surfsense_web/components/announcements/AnnouncementsDialog.tsx
delete mode 100644 surfsense_web/components/layout/ui/sidebar/AnnouncementsSidebar.tsx
diff --git a/surfsense_web/app/(home)/announcements/layout.tsx b/surfsense_web/app/(home)/announcements/layout.tsx
index 072db2c3f..cff15d3a1 100644
--- a/surfsense_web/app/(home)/announcements/layout.tsx
+++ b/surfsense_web/app/(home)/announcements/layout.tsx
@@ -2,20 +2,20 @@ import type { Metadata } from "next";
import type { ReactNode } from "react";
export const metadata: Metadata = {
- title: "Announcements | SurfSense",
+ title: "What's New | SurfSense",
description: "Latest product updates, feature releases, and news from SurfSense.",
alternates: {
canonical: "https://surfsense.com/announcements",
},
openGraph: {
- title: "Announcements | SurfSense",
+ title: "What's New | SurfSense",
description: "Latest product updates, feature releases, and news from SurfSense.",
url: "https://surfsense.com/announcements",
type: "website",
},
twitter: {
card: "summary_large_image",
- title: "Announcements | SurfSense",
+ title: "What's New | SurfSense",
description: "Latest product updates, feature releases, and news from SurfSense.",
},
};
diff --git a/surfsense_web/app/(home)/announcements/page.tsx b/surfsense_web/app/(home)/announcements/page.tsx
index 966c09f77..f287e43d1 100644
--- a/surfsense_web/app/(home)/announcements/page.tsx
+++ b/surfsense_web/app/(home)/announcements/page.tsx
@@ -24,7 +24,7 @@ export default function AnnouncementsPage() {
- Announcements
+ What's New
diff --git a/surfsense_web/atoms/settings/settings-dialog.atoms.ts b/surfsense_web/atoms/settings/settings-dialog.atoms.ts
index 3b49f1f06..480c41204 100644
--- a/surfsense_web/atoms/settings/settings-dialog.atoms.ts
+++ b/surfsense_web/atoms/settings/settings-dialog.atoms.ts
@@ -21,3 +21,5 @@ export const userSettingsDialogAtom = atom
({
});
export const teamDialogAtom = atom(false);
+
+export const announcementsDialogAtom = atom(false);
diff --git a/surfsense_web/components/announcements/AnnouncementsDialog.tsx b/surfsense_web/components/announcements/AnnouncementsDialog.tsx
new file mode 100644
index 000000000..4d3aaeb73
--- /dev/null
+++ b/surfsense_web/components/announcements/AnnouncementsDialog.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useAtom } from "jotai";
+import { useEffect } from "react";
+import { announcementsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
+import { AnnouncementCard } from "@/components/announcements/AnnouncementCard";
+import { AnnouncementsEmptyState } from "@/components/announcements/AnnouncementsEmptyState";
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
+import { Separator } from "@/components/ui/separator";
+import { useAnnouncements } from "@/hooks/use-announcements";
+
+export function AnnouncementsDialog() {
+ const [open, setOpen] = useAtom(announcementsDialogAtom);
+ const { announcements, markAllRead } = useAnnouncements();
+
+ // Auto-mark all visible announcements as read when the dialog opens
+ useEffect(() => {
+ if (open) {
+ markAllRead();
+ }
+ }, [open, markAllRead]);
+
+ return (
+
+ );
+}
diff --git a/surfsense_web/components/announcements/AnnouncementsEmptyState.tsx b/surfsense_web/components/announcements/AnnouncementsEmptyState.tsx
index 329a284db..7aa4564b6 100644
--- a/surfsense_web/components/announcements/AnnouncementsEmptyState.tsx
+++ b/surfsense_web/components/announcements/AnnouncementsEmptyState.tsx
@@ -6,9 +6,9 @@ export function AnnouncementsEmptyState() {
- No announcements
+ Nothing new yet
- You're all caught up! New announcements will appear here.
+ You're all caught up! New updates will appear here.
);
diff --git a/surfsense_web/components/homepage/footer-new.tsx b/surfsense_web/components/homepage/footer-new.tsx
index 53eb833e9..5943cc8ec 100644
--- a/surfsense_web/components/homepage/footer-new.tsx
+++ b/surfsense_web/components/homepage/footer-new.tsx
@@ -38,7 +38,7 @@ export function FooterNew() {
href: "/contact",
},
{
- title: "Announcements",
+ title: "What's New",
href: "/announcements",
},
];
diff --git a/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx b/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx
index 8b362f45b..a2695078a 100644
--- a/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Inbox, Megaphone, SquareLibrary } from "lucide-react";
+import { Inbox, SquareLibrary } from "lucide-react";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
@@ -55,28 +55,24 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
const navItems: NavItem[] = useMemo(
() =>
- [
- {
- title: "Inbox",
- url: "#inbox",
- icon: Inbox,
- isActive: false,
- },
- isMobile
- ? {
- title: "Documents",
- url: "#documents",
- icon: SquareLibrary,
- isActive: false,
- }
- : null,
- {
- title: "Announcements",
- url: "#announcements",
- icon: Megaphone,
- isActive: false,
- },
- ].filter((item): item is NavItem => item !== null),
+ (
+ [
+ {
+ title: "Inbox",
+ url: "#inbox",
+ icon: Inbox,
+ isActive: false,
+ },
+ isMobile
+ ? {
+ title: "Documents",
+ url: "#documents",
+ icon: SquareLibrary,
+ isActive: false,
+ }
+ : null,
+ ] as (NavItem | null)[]
+ ).filter((item): item is NavItem => item !== null),
[isMobile]
);
@@ -90,11 +86,12 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
(item: NavItem) => {
if (item.title === "Inbox") gate("use the inbox");
else if (item.title === "Documents") setIsDocsSidebarOpen((v) => !v);
- else if (item.title === "Announcements") gate("view announcements");
},
[gate]
);
+ const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]);
+
const handleSearchSpaceSelect = useCallback(
(_id: number) => gate("switch search spaces"),
[gate]
@@ -127,6 +124,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
onSettings={gatedAction("search space settings")}
onManageMembers={gatedAction("team management")}
onUserSettings={gatedAction("account settings")}
+ onAnnouncements={handleAnnouncements}
onLogout={() => router.push("/register")}
pageUsage={pageUsage}
isChatPage
diff --git a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
index c4b127c63..fdc4930d6 100644
--- a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
+++ b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
@@ -23,6 +23,8 @@ interface IconRailProps {
onNavItemClick?: (item: NavItem) => void;
user: User;
onUserSettings?: () => void;
+ onAnnouncements?: () => void;
+ announcementUnreadCount?: number;
onLogout?: () => void;
theme?: string;
setTheme?: (theme: "light" | "dark" | "system") => void;
@@ -42,6 +44,8 @@ export function IconRail({
onNavItemClick,
user,
onUserSettings,
+ onAnnouncements,
+ announcementUnreadCount = 0,
onLogout,
theme,
setTheme,
@@ -138,6 +142,8 @@ export function IconRail({
Promise;
}
-export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | "announcements" | null;
+export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | null;
// Inbox-related props — per-tab data sources with independent loading/pagination
interface InboxProps {
@@ -88,6 +87,8 @@ interface LayoutShellProps {
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
+ onAnnouncements?: () => void;
+ announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
@@ -189,6 +190,8 @@ export function LayoutShell({
onSettings,
onManageMembers,
onUserSettings,
+ onAnnouncements,
+ announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
@@ -237,9 +240,7 @@ export function LayoutShell({
? "Shared Chats"
: activeSlideoutPanel === "private"
? "Private Chats"
- : activeSlideoutPanel === "announcements"
- ? "Announcements"
- : "Panel";
+ : "Panel";
// Mobile layout
if (isMobile) {
@@ -277,6 +278,8 @@ export function LayoutShell({
onSettings={onSettings}
onManageMembers={onManageMembers}
onUserSettings={onUserSettings}
+ onAnnouncements={onAnnouncements}
+ announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
pageUsage={pageUsage}
theme={theme}
@@ -313,21 +316,6 @@ export function LayoutShell({
/>
)}
- {activeSlideoutPanel === "announcements" && (
-
- closeSlideout(open)}
- onCloseMobileSidebar={() => setMobileMenuOpen(false)}
- />
-
- )}
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
)}
- {activeSlideoutPanel === "announcements" && (
-
- closeSlideout(open)} />
-
- )}
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
void;
- onCloseMobileSidebar?: () => void;
-}
-
-interface AnnouncementsSidebarProps extends AnnouncementsSidebarContentProps {
- open: boolean;
-}
-
-export function AnnouncementsSidebarContent({
- onOpenChange,
- onCloseMobileSidebar,
-}: AnnouncementsSidebarContentProps) {
- const isMobile = !useMediaQuery("(min-width: 640px)");
- const { announcements, markAllRead } = useAnnouncements();
-
- useEffect(() => {
- markAllRead();
- }, [markAllRead]);
-
- return (
-
-
-
-
- {isMobile && (
-
- )}
-
Announcements
-
-
-
-
-
- {announcements.length === 0 ? (
-
- ) : (
-
- {announcements.map((announcement) => (
-
- ))}
-
- )}
-
-
- );
-}
-
-export function AnnouncementsSidebar({
- open,
- onOpenChange,
- onCloseMobileSidebar,
-}: AnnouncementsSidebarProps) {
- return (
-
-
-
- );
-}
diff --git a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx
index def40f24f..ebb12bae8 100644
--- a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx
@@ -34,6 +34,8 @@ interface MobileSidebarProps {
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
+ onAnnouncements?: () => void;
+ announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
@@ -77,6 +79,8 @@ export function MobileSidebar({
onSettings,
onManageMembers,
onUserSettings,
+ onAnnouncements,
+ announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
@@ -193,6 +197,15 @@ export function MobileSidebar({
}
: undefined
}
+ onAnnouncements={
+ onAnnouncements
+ ? () => {
+ onOpenChange(false);
+ onAnnouncements();
+ }
+ : undefined
+ }
+ announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
pageUsage={pageUsage}
theme={theme}
diff --git a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx
index 658067f3f..3110382d0 100644
--- a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx
@@ -139,10 +139,6 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
return (
{items.map((item) => {
- const joyrideAttr =
- item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
- ? { "data-joyride": "inbox-sidebar" as const }
- : {};
const { tooltip } = getStatusInfo(item.statusIndicator);
return (
@@ -159,12 +155,11 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
}
trailingContent={
}
tooltipContent={tooltip}
- buttonProps={joyrideAttr}
/>
);
})}
diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
index c5990b11b..946079333 100644
--- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
@@ -4,7 +4,7 @@ import { CreditCard, SquarePen, Zap } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
@@ -53,6 +53,8 @@ interface SidebarProps {
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
+ onAnnouncements?: () => void;
+ announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
@@ -87,6 +89,8 @@ export function Sidebar({
onSettings,
onManageMembers,
onUserSettings,
+ onAnnouncements,
+ announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
@@ -101,6 +105,14 @@ export function Sidebar({
const t = useTranslations("sidebar");
const [openDropdownChatId, setOpenDropdownChatId] = useState
(null);
+ // Inbox is rendered explicitly right below New Chat. Pull it out of the
+ // nav items list so it doesn't also appear in the bottom NavSection.
+ const inboxItem = useMemo(() => navItems.find((item) => item.url === "#inbox"), [navItems]);
+ const footerNavItems = useMemo(
+ () => navItems.filter((item) => item.url !== "#inbox"),
+ [navItems]
+ );
+
return (
)}
- {/* New chat button */}
+ {/* New chat button + Inbox */}
+ {inboxItem && (
+ onNavItemClick?.(inboxItem)}
+ isCollapsed={isCollapsed}
+ isActive={inboxItem.isActive}
+ badge={inboxItem.badge}
+ buttonProps={
+ {
+ "data-joyride": "inbox-sidebar",
+ } as React.ButtonHTMLAttributes
+ }
+ />
+ )}
{/* Chat sections - fills available space */}
@@ -271,8 +298,12 @@ export function Sidebar({
{/* Footer */}
{/* Platform navigation */}
- {navItems.length > 0 && (
-
+ {footerNavItems.length > 0 && (
+
)}
@@ -281,6 +312,8 @@ export function Sidebar({
void;
+ onAnnouncements?: () => void;
+ announcementUnreadCount?: number;
onLogout?: () => void;
isCollapsed?: boolean;
theme?: string;
setTheme?: (theme: "light" | "dark" | "system") => void;
}
+function formatAnnouncementCount(count: number): string {
+ if (count <= 999) {
+ return count.toString();
+ }
+ const thousands = Math.floor(count / 1000);
+ return `${thousands}k+`;
+}
+
/**
* Generates a consistent color based on email
*/
@@ -152,6 +163,8 @@ function UserAvatar({
export function SidebarUserProfile({
user,
onUserSettings,
+ onAnnouncements,
+ announcementUnreadCount = 0,
onLogout,
isCollapsed = false,
theme,
@@ -228,6 +241,18 @@ export function SidebarUserProfile({
{t("user_settings")}
+ {onAnnouncements && (
+
+
+ What's New
+ {announcementUnreadCount > 0 && (
+
+ {formatAnnouncementCount(announcementUnreadCount)}
+
+ )}
+
+ )}
+
{setTheme && (
@@ -382,6 +407,18 @@ export function SidebarUserProfile({
{t("user_settings")}
+ {onAnnouncements && (
+
+
+ What's New
+ {announcementUnreadCount > 0 && (
+
+ {formatAnnouncementCount(announcementUnreadCount)}
+
+ )}
+
+ )}
+
{setTheme && (
diff --git a/surfsense_web/components/layout/ui/sidebar/index.ts b/surfsense_web/components/layout/ui/sidebar/index.ts
index 33023c9e8..d72f86c8a 100644
--- a/surfsense_web/components/layout/ui/sidebar/index.ts
+++ b/surfsense_web/components/layout/ui/sidebar/index.ts
@@ -1,6 +1,5 @@
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
-export { AnnouncementsSidebar, AnnouncementsSidebarContent } from "./AnnouncementsSidebar";
export { ChatListItem } from "./ChatListItem";
export { DocumentsSidebar } from "./DocumentsSidebar";
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
diff --git a/surfsense_web/lib/announcements/announcements-data.ts b/surfsense_web/lib/announcements/announcements-data.ts
index bde8b3f43..ce44ec539 100644
--- a/surfsense_web/lib/announcements/announcements-data.ts
+++ b/surfsense_web/lib/announcements/announcements-data.ts
@@ -15,8 +15,8 @@ import type { Announcement } from "@/contracts/types/announcement.types";
export const announcements: Announcement[] = [
{
id: "announcement-1",
- title: "Introducing Announcements",
- description: "All major announcements will be posted here.",
+ title: "Introducing What's New",
+ description: "All major product updates will be posted here.",
category: "feature",
date: "2026-02-17T00:00:00Z",
startTime: "2026-02-17T00:00:00Z",
From 89beced3c0f68c0e52b439e5e8ea63c465e0639c Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 3 May 2026 18:47:01 +0530
Subject: [PATCH 11/76] feat(layout): integrate announcement unread count into
FreeLayoutDataProvider for enhanced user notifications
---
.../components/layout/providers/FreeLayoutDataProvider.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx b/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx
index a2695078a..68ee552f8 100644
--- a/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx
@@ -6,6 +6,7 @@ import type { ReactNode } from "react";
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { useAnonymousMode } from "@/contexts/anonymous-mode";
import { useLoginGate } from "@/contexts/login-gate";
+import { useAnnouncements } from "@/hooks/use-announcements";
import { useIsMobile } from "@/hooks/use-mobile";
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
import type { ChatItem, NavItem, PageUsage, SearchSpace } from "../types/layout.types";
@@ -28,6 +29,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
const { gate } = useLoginGate();
const anonMode = useAnonymousMode();
const isMobile = useIsMobile();
+ const { unreadCount: announcementUnreadCount } = useAnnouncements();
const [quota, setQuota] = useState<{ used: number; limit: number } | null>(null);
const [isDocsSidebarOpen, setIsDocsSidebarOpen] = useState(false);
@@ -125,6 +127,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
onManageMembers={gatedAction("team management")}
onUserSettings={gatedAction("account settings")}
onAnnouncements={handleAnnouncements}
+ announcementUnreadCount={announcementUnreadCount}
onLogout={() => router.push("/register")}
pageUsage={pageUsage}
isChatPage
From 3eb1cc9cf68115e38ed7dfaf87f05e5723b0366b Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 3 May 2026 18:56:58 +0530
Subject: [PATCH 12/76] refactor(icon-rail, tabs): remove unused separators and
improve comments for clarity in SearchSpaceAvatar and TabBar components
---
.../layout/ui/icon-rail/SearchSpaceAvatar.tsx | 8 ++------
surfsense_web/components/layout/ui/tabs/TabBar.tsx | 10 ++++------
2 files changed, 6 insertions(+), 12 deletions(-)
diff --git a/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx b/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
index 641e5f5af..c35d8c708 100644
--- a/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
+++ b/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
@@ -7,14 +7,12 @@ import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
- ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@@ -157,7 +155,6 @@ export function SearchSpaceAvatar({
{tCommon("settings")}
)}
- {onSettings && onDelete && }
{onDelete && isOwner && (
@@ -190,7 +187,7 @@ export function SearchSpaceAvatar({
{avatarButton}
-
{menuItems}
+
{menuItems}
);
}
@@ -208,14 +205,13 @@ export function SearchSpaceAvatar({
{tooltipContent}
-
+
{onSettings && (
{tCommon("settings")}
)}
- {onSettings && onDelete && }
{onDelete && isOwner && (
diff --git a/surfsense_web/components/layout/ui/tabs/TabBar.tsx b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
index f3c88bf6a..cefb79e43 100644
--- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx
+++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx
@@ -20,7 +20,7 @@ interface TabBarProps {
className?: string;
}
-// Pure scroll-target calculation (port of opencode's nextTabListScrollLeft).
+// Pure scroll-target calculation for the tab list.
// - When the list shrinks (a tab was closed), do not move the scroll.
// - When the list overflows after growing, snap to the right edge so the new tab is visible.
function nextTabListScrollLeft(input: {
@@ -66,9 +66,8 @@ export function TabBar({
[closeTab, onTabSwitch]
);
- // React to tab list growth (port of opencode's createFileTabListSync).
- // Uses a MutationObserver instead of a tab-id effect so the scroll catches
- // the moment the new tab is added to the DOM, not after activation lands.
+ // React to tab list growth via a MutationObserver so the scroll catches the
+ // moment a new tab is added to the DOM, not after activation lands.
// Also remaps vertical wheel motion to horizontal scroll.
useEffect(() => {
const el = scrollRef.current;
@@ -195,8 +194,7 @@ export function TabBar({
.sticky` rule in tabs.css.
+ // + button get visually masked into the bar's background.
"sticky right-0 z-10 flex h-full shrink-0 items-center bg-main-panel pl-3 pr-1",
"before:content-[''] before:absolute before:inset-y-0 before:-left-4 before:w-4 before:pointer-events-none",
"before:bg-gradient-to-r before:from-transparent before:to-main-panel"
From 2e1367b00e0c354840a61796eeef55b8adb0aadb Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 3 May 2026 19:18:30 +0530
Subject: [PATCH 13/76] fix(sidebar): adjust padding and gradient heights for
improved layout consistency; enhance SidebarUsageFooter to conditionally
render border based on navigation items
---
.../components/layout/ui/sidebar/Sidebar.tsx | 27 +++++++++++++------
.../layout/ui/sidebar/SidebarSection.tsx | 2 +-
2 files changed, 20 insertions(+), 9 deletions(-)
diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
index 946079333..3f02f1d6f 100644
--- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx
@@ -179,7 +179,7 @@ export function Sidebar({
{isCollapsed ? (
) : (
-
+
{/* Shared Chats Section - takes only space needed, max 50% */}
0 ? (
4 ? "pb-8" : ""}`}
+ className={`flex flex-col gap-0.5 max-h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-2" : ""}`}
>
{sharedChats.slice(0, 20).map((chat) => (
{/* Gradient fade indicator when more than 4 items */}
{sharedChats.length > 4 && (
-
+
)}
) : (
@@ -266,7 +266,7 @@ export function Sidebar({
) : chats.length > 0 ? (
4 ? "pb-8" : ""}`}
+ className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${chats.length > 4 ? "pb-2" : ""}`}
>
{chats.slice(0, 20).map((chat) => (
{/* Gradient fade indicator when more than 4 items */}
{chats.length > 4 && (
-
+
)}
) : (
@@ -306,7 +306,11 @@ export function Sidebar({
/>
)}
-
+
0}
+ />
{renderUserProfile && (
+
{pageUsage && (
@@ -373,7 +384,7 @@ function SidebarUsageFooter({
}
return (
-
+
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarSection.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarSection.tsx
index aeb0ebef1..10d5942ca 100644
--- a/surfsense_web/components/layout/ui/sidebar/SidebarSection.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/SidebarSection.tsx
@@ -69,7 +69,7 @@ export function SidebarSection({
-
+
{children}
From cdce6a8f147864b8efc441f31a80da4f63927bf5 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 3 May 2026 20:38:21 +0530
Subject: [PATCH 14/76] refactor(styles): unify panel and sidebar colors for
consistent theming; update hover effects for buttons in the right panel and
sidebar
---
surfsense_web/app/globals.css | 24 +++++++++++++------
.../components/assistant-ui/user-message.tsx | 2 +-
.../layout/ui/right-panel/RightPanel.tsx | 6 ++---
.../layout/ui/shell/LayoutShell.tsx | 24 ++++++++++++-------
.../components/layout/ui/sidebar/Sidebar.tsx | 6 ++---
.../ui/sidebar/SidebarCollapseButton.tsx | 2 +-
.../components/layout/ui/tabs/TabBar.tsx | 6 ++---
7 files changed, 44 insertions(+), 26 deletions(-)
diff --git a/surfsense_web/app/globals.css b/surfsense_web/app/globals.css
index a37ddb8f3..76e3eea8c 100644
--- a/surfsense_web/app/globals.css
+++ b/surfsense_web/app/globals.css
@@ -39,7 +39,11 @@
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
+ /* Unified surface used by the left sidebar, main panel, and right panel. */
+ --panel: oklch(0.96 0 0);
+ /* Distinct (lighter) surface used by the leftmost icon rail. */
+ --rail: oklch(0.985 0 0);
+ --sidebar: var(--panel);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
@@ -47,7 +51,7 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
- --main-panel: oklch(1 0 0);
+ --main-panel: var(--panel);
--syntax-bg: #f5f5f5;
--brand: oklch(0.623 0.214 259.815);
--highlight: oklch(0.852 0.199 91.936);
@@ -70,23 +74,27 @@
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
- --border: oklch(0.269 0 0);
- --input: oklch(0.269 0 0);
+ --border: oklch(0.32 0 0);
+ --input: oklch(0.32 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
+ /* Unified surface used by the left sidebar, main panel, and right panel. */
+ --panel: oklch(0.24 0 0);
+ /* Distinct (slightly darker) surface used by the leftmost icon rail. */
+ --rail: oklch(0.205 0 0);
+ --sidebar: var(--panel);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(0.269 0 0);
+ --sidebar-border: oklch(0.32 0 0);
--sidebar-ring: oklch(0.439 0 0);
- --main-panel: oklch(0.18 0 0);
+ --main-panel: var(--panel);
--syntax-bg: #1e1e1e;
--brand: oklch(0.707 0.165 254.624);
--highlight: oklch(0.852 0.199 91.936);
@@ -118,6 +126,8 @@
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-main-panel: var(--main-panel);
+ --color-panel: var(--panel);
+ --color-rail: var(--rail);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
diff --git a/surfsense_web/components/assistant-ui/user-message.tsx b/surfsense_web/components/assistant-ui/user-message.tsx
index 145ac2d7e..b28ee374e 100644
--- a/surfsense_web/components/assistant-ui/user-message.tsx
+++ b/surfsense_web/components/assistant-ui/user-message.tsx
@@ -102,7 +102,7 @@ export const UserMessage: FC = () => {
-
+
diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
index 3dc7a96d4..1e543c885 100644
--- a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
+++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
@@ -61,7 +61,7 @@ function CollapseButton({ onClick }: { onClick: () => void }) {
variant="ghost"
size="icon"
onClick={onClick}
- className="h-8 w-8 shrink-0 text-muted-foreground hover:text-muted-foreground hover:bg-muted/40"
+ className="h-8 w-8 shrink-0 text-muted-foreground hover:text-muted-foreground hover:bg-accent"
>
Collapse panel
@@ -102,7 +102,7 @@ export function RightPanelExpandButton() {
variant="ghost"
size="icon"
onClick={() => startTransition(() => setCollapsed(false))}
- className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:text-muted-foreground hover:bg-muted/40"
+ className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:text-muted-foreground hover:bg-accent"
>
Expand panel
@@ -203,7 +203,7 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
return (