From 64e118befd47e77f477032e46190892829fa0d7e Mon Sep 17 00:00:00 2001 From: Eric Lammertsma Date: Mon, 9 Feb 2026 10:48:43 -0500 Subject: [PATCH 1/5] feat: sidebar resizing with mouse drag support --- .../layout/hooks/useSidebarResize.ts | 101 ++++++++++++++++++ .../components/layout/ui/sidebar/Sidebar.tsx | 15 ++- 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 surfsense_web/components/layout/hooks/useSidebarResize.ts diff --git a/surfsense_web/components/layout/hooks/useSidebarResize.ts b/surfsense_web/components/layout/hooks/useSidebarResize.ts new file mode 100644 index 000000000..887c86dce --- /dev/null +++ b/surfsense_web/components/layout/hooks/useSidebarResize.ts @@ -0,0 +1,101 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; + +const SIDEBAR_WIDTH_COOKIE_NAME = "sidebar_width"; +const SIDEBAR_WIDTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year + +export const SIDEBAR_MIN_WIDTH = 240; +export const SIDEBAR_MAX_WIDTH = 480; + +interface UseSidebarResizeReturn { + sidebarWidth: number; + handleMouseDown: (e: React.MouseEvent) => void; + isDragging: boolean; +} + +export function useSidebarResize(defaultWidth = SIDEBAR_MIN_WIDTH): UseSidebarResizeReturn { + const [sidebarWidth, setSidebarWidth] = useState(defaultWidth); + const [isDragging, setIsDragging] = useState(false); + + const startXRef = useRef(0); + const startWidthRef = useRef(defaultWidth); + + // Initialize from cookie on mount + useEffect(() => { + try { + const match = document.cookie.match(/(?:^|; )sidebar_width=([^;]+)/); + if (match) { + const parsed = Number(match[1]); + if (!Number.isNaN(parsed) && parsed >= SIDEBAR_MIN_WIDTH && parsed <= SIDEBAR_MAX_WIDTH) { + setSidebarWidth(parsed); + } + } + } catch { + // Ignore cookie read errors + } + }, []); + + // Persist width to cookie + const persistWidth = useCallback((width: number) => { + try { + document.cookie = `${SIDEBAR_WIDTH_COOKIE_NAME}=${width}; path=/; max-age=${SIDEBAR_WIDTH_COOKIE_MAX_AGE}`; + } catch { + // Ignore cookie write errors + } + }, []); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + startXRef.current = e.clientX; + startWidthRef.current = sidebarWidth; + setIsDragging(true); + + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [sidebarWidth] + ); + + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + const delta = e.clientX - startXRef.current; + const newWidth = Math.min( + SIDEBAR_MAX_WIDTH, + Math.max(SIDEBAR_MIN_WIDTH, startWidthRef.current + delta) + ); + setSidebarWidth(newWidth); + }; + + const handleMouseUp = () => { + setIsDragging(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + + // Persist the final width + setSidebarWidth((currentWidth) => { + persistWidth(currentWidth); + return currentWidth; + }); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + }, [isDragging, persistWidth]); + + return { + sidebarWidth, + handleMouseDown, + isDragging, + }; +} diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index 883fa5890..6b0c9fe7a 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -7,6 +7,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types"; +import { useSidebarResize } from "../../hooks/useSidebarResize"; import { ChatListItem } from "./ChatListItem"; import { NavSection } from "./NavSection"; import { PageUsageDisplay } from "./PageUsageDisplay"; @@ -82,15 +83,25 @@ export function Sidebar({ disableTooltips = false, }: SidebarProps) { const t = useTranslations("sidebar"); + const { sidebarWidth, handleMouseDown, isDragging } = useSidebarResize(); return (
+ {/* Resize handle on right border */} + {!isCollapsed && ( +
+ )} {/* Header - search space name or collapse button when collapsed */} {isCollapsed ? (
From ed5f0d10e81e05aec1b6d525506ebc33072b61cc Mon Sep 17 00:00:00 2001 From: Eric Lammertsma Date: Mon, 9 Feb 2026 11:13:56 -0500 Subject: [PATCH 2/5] fix: made inbox respect resizable sidebar width --- .../components/layout/hooks/SidebarContext.tsx | 1 + .../components/layout/ui/shell/LayoutShell.tsx | 9 +++++++-- .../components/layout/ui/sidebar/InboxSidebar.tsx | 6 +++--- .../components/layout/ui/sidebar/Sidebar.tsx | 15 ++++++++++----- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/surfsense_web/components/layout/hooks/SidebarContext.tsx b/surfsense_web/components/layout/hooks/SidebarContext.tsx index 35f76929d..bfb5b5aeb 100644 --- a/surfsense_web/components/layout/hooks/SidebarContext.tsx +++ b/surfsense_web/components/layout/hooks/SidebarContext.tsx @@ -6,6 +6,7 @@ interface SidebarContextValue { isCollapsed: boolean; setIsCollapsed: (collapsed: boolean) => void; toggleCollapsed: () => void; + sidebarWidth: number; } const SidebarContext = createContext(null); diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx index 3a8255e7a..58f407494 100644 --- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx +++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx @@ -6,6 +6,7 @@ import type { InboxItem } from "@/hooks/use-inbox"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { SidebarProvider, useSidebarState } from "../../hooks"; +import { useSidebarResize } from "../../hooks/useSidebarResize"; import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types"; import { Header } from "../header"; import { IconRail } from "../icon-rail"; @@ -116,11 +117,12 @@ export function LayoutShell({ const isMobile = useIsMobile(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed); + const { sidebarWidth, handleMouseDown: onResizeMouseDown, isDragging: isResizing } = useSidebarResize(); // Memoize context value to prevent unnecessary re-renders const sidebarContextValue = useMemo( - () => ({ isCollapsed, setIsCollapsed, toggleCollapsed }), - [isCollapsed, setIsCollapsed, toggleCollapsed] + () => ({ isCollapsed, setIsCollapsed, toggleCollapsed, sidebarWidth }), + [isCollapsed, setIsCollapsed, toggleCollapsed, sidebarWidth] ); // Mobile layout @@ -236,6 +238,9 @@ export function LayoutShell({ setTheme={setTheme} className="hidden md:flex border-r shrink-0" isLoadingChats={isLoadingChats} + sidebarWidth={sidebarWidth} + onResizeMouseDown={onResizeMouseDown} + isResizing={isResizing} /> {/* Docked Inbox Sidebar - renders as flex sibling between sidebar and content */} diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index b6caed330..5a709a8dd 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -61,9 +61,8 @@ import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cn } from "@/lib/utils"; import { useSidebarContextSafe } from "../../hooks"; -// Sidebar width constants +// Sidebar width constant for collapsed state const SIDEBAR_COLLAPSED_WIDTH = 60; -const SIDEBAR_EXPANDED_WIDTH = 240; /** * Get initials from name or email for avatar fallback @@ -566,7 +565,8 @@ export function InboxSidebar({ const isCollapsed = sidebarContext?.isCollapsed ?? false; // Calculate the left position for the inbox panel (relative to sidebar) - const sidebarWidth = isCollapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_EXPANDED_WIDTH; + // Use dynamic width from context (tracks resize) for expanded state + const sidebarWidth = isCollapsed ? SIDEBAR_COLLAPSED_WIDTH : (sidebarContext?.sidebarWidth ?? 240); if (!mounted) return null; diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index 6b0c9fe7a..f5589ba1c 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -7,7 +7,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types"; -import { useSidebarResize } from "../../hooks/useSidebarResize"; +import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize"; import { ChatListItem } from "./ChatListItem"; import { NavSection } from "./NavSection"; import { PageUsageDisplay } from "./PageUsageDisplay"; @@ -52,6 +52,9 @@ interface SidebarProps { className?: string; isLoadingChats?: boolean; disableTooltips?: boolean; + sidebarWidth?: number; + onResizeMouseDown?: (e: React.MouseEvent) => void; + isResizing?: boolean; } export function Sidebar({ @@ -81,24 +84,26 @@ export function Sidebar({ className, isLoadingChats = false, disableTooltips = false, + sidebarWidth = SIDEBAR_MIN_WIDTH, + onResizeMouseDown, + isResizing = false, }: SidebarProps) { const t = useTranslations("sidebar"); - const { sidebarWidth, handleMouseDown, isDragging } = useSidebarResize(); return (
{/* Resize handle on right border */} - {!isCollapsed && ( + {!isCollapsed && onResizeMouseDown && (
)} From 4005e03ec4faee0a28ad7865e9e499c5bfe13ed2 Mon Sep 17 00:00:00 2001 From: Eric Lammertsma Date: Mon, 9 Feb 2026 11:41:55 -0500 Subject: [PATCH 3/5] refactor: consolidated secondary sidebars into a reusable slide-out panel --- .../layout/providers/LayoutDataProvider.tsx | 39 +++++---- .../layout/ui/shell/LayoutShell.tsx | 32 +++++++- .../ui/sidebar/AllPrivateChatsSidebar.tsx | 61 +++----------- .../ui/sidebar/AllSharedChatsSidebar.tsx | 61 +++----------- .../layout/ui/sidebar/InboxSidebar.tsx | 61 +------------- .../ui/sidebar/SidebarSlideOutPanel.tsx | 82 +++++++++++++++++++ 6 files changed, 158 insertions(+), 178 deletions(-) create mode 100644 surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index ead017a3e..e40fe28ad 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -33,9 +33,6 @@ import { cacheKeys } from "@/lib/query-client/cache-keys"; import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types"; import { CreateSearchSpaceDialog } from "../ui/dialogs"; import { LayoutShell } from "../ui/shell"; -import { AllPrivateChatsSidebar } from "../ui/sidebar/AllPrivateChatsSidebar"; -import { AllSharedChatsSidebar } from "../ui/sidebar/AllSharedChatsSidebar"; - interface LayoutDataProviderProps { searchSpaceId: string; children: React.ReactNode; @@ -390,7 +387,13 @@ export function LayoutDataProvider({ (item: NavItem) => { // Handle inbox specially - toggle sidebar instead of navigating if (item.url === "#inbox") { - setIsInboxSidebarOpen((prev) => !prev); + setIsInboxSidebarOpen((prev) => { + if (!prev) { + setIsAllSharedChatsSidebarOpen(false); + setIsAllPrivateChatsSidebarOpen(false); + } + return !prev; + }); return; } router.push(item.url); @@ -490,10 +493,14 @@ export function LayoutDataProvider({ const handleViewAllSharedChats = useCallback(() => { setIsAllSharedChatsSidebarOpen(true); + setIsAllPrivateChatsSidebarOpen(false); + setIsInboxSidebarOpen(false); }, []); const handleViewAllPrivateChats = useCallback(() => { setIsAllPrivateChatsSidebarOpen(true); + setIsAllSharedChatsSidebarOpen(false); + setIsInboxSidebarOpen(false); }, []); // Delete handlers @@ -614,6 +621,16 @@ export function LayoutDataProvider({ isDocked: isInboxDocked, onDockedChange: setIsInboxDocked, }} + allSharedChatsPanel={{ + open: isAllSharedChatsSidebarOpen, + onOpenChange: setIsAllSharedChatsSidebarOpen, + searchSpaceId, + }} + allPrivateChatsPanel={{ + open: isAllPrivateChatsSidebarOpen, + onOpenChange: setIsAllPrivateChatsSidebarOpen, + searchSpaceId, + }} > {children} @@ -796,20 +813,6 @@ export function LayoutDataProvider({ - {/* All Shared Chats Sidebar */} - - - {/* All Private Chats Sidebar */} - - {/* Create Search Space Dialog */} void; searchSpaceId: string }; + allPrivateChatsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string }; } export function LayoutShell({ @@ -113,6 +123,8 @@ export function LayoutShell({ className, inbox, isLoadingChats = false, + allSharedChatsPanel, + allPrivateChatsPanel, }: LayoutShellProps) { const isMobile = useIsMobile(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); @@ -280,6 +292,24 @@ export function LayoutShell({ onDockedChange={inbox.onDockedChange} /> )} + + {/* All Shared Chats - slide-out panel */} + {allSharedChatsPanel && ( + + )} + + {/* All Private Chats - slide-out panel */} + {allPrivateChatsPanel && ( + + )}
diff --git a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx index 1d4f590bd..c78adfef1 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx @@ -12,11 +12,9 @@ import { User, X, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -40,6 +38,7 @@ import { updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; +import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; interface AllPrivateChatsSidebarProps { open: boolean; @@ -69,16 +68,11 @@ export function AllPrivateChatsSidebar({ const [archivingThreadId, setArchivingThreadId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showArchived, setShowArchived] = useState(false); - const [mounted, setMounted] = useState(false); const [openDropdownId, setOpenDropdownId] = useState(null); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const isSearchMode = !!debouncedSearchQuery.trim(); - useEffect(() => { - setMounted(true); - }, []); - useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { @@ -89,17 +83,6 @@ export function AllPrivateChatsSidebar({ return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - const { data: threadsData, error: threadsError, @@ -214,33 +197,13 @@ export function AllPrivateChatsSidebar({ const activeCount = activeChats.length; const archivedCount = archivedChats.length; - if (!mounted) return null; - - return createPortal( - - {open && ( - <> - onOpenChange(false)} - aria-hidden="true" - /> - - -
+ return ( + +

{t("chats") || "Private Chats"}

@@ -451,11 +414,7 @@ export function AllPrivateChatsSidebar({ )}
)} -
- - - )} - , - document.body +
+ ); } diff --git a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx index db7ca73b2..ed22b819d 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx @@ -12,11 +12,9 @@ import { Users, X, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -40,6 +38,7 @@ import { updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; +import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; interface AllSharedChatsSidebarProps { open: boolean; @@ -69,16 +68,11 @@ export function AllSharedChatsSidebar({ const [archivingThreadId, setArchivingThreadId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showArchived, setShowArchived] = useState(false); - const [mounted, setMounted] = useState(false); const [openDropdownId, setOpenDropdownId] = useState(null); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const isSearchMode = !!debouncedSearchQuery.trim(); - useEffect(() => { - setMounted(true); - }, []); - useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { @@ -89,17 +83,6 @@ export function AllSharedChatsSidebar({ return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - const { data: threadsData, error: threadsError, @@ -214,33 +197,13 @@ export function AllSharedChatsSidebar({ const activeCount = activeChats.length; const archivedCount = archivedChats.length; - if (!mounted) return null; - - return createPortal( - - {open && ( - <> - onOpenChange(false)} - aria-hidden="true" - /> - - -
+ return ( + +

{t("shared_chats") || "Shared Chats"}

@@ -451,11 +414,7 @@ export function AllSharedChatsSidebar({ )}
)} -
- - - )} - , - document.body +
+ ); } diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index 5a709a8dd..c459141d6 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -19,7 +19,6 @@ import { Search, X, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -59,10 +58,7 @@ import { useMediaQuery } from "@/hooks/use-media-query"; import { notificationsApiService } from "@/lib/apis/notifications-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cn } from "@/lib/utils"; -import { useSidebarContextSafe } from "../../hooks"; - -// Sidebar width constant for collapsed state -const SIDEBAR_COLLAPSED_WIDTH = 60; +import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; /** * Get initials from name or email for avatar fallback @@ -560,14 +556,6 @@ export function InboxSidebar({ }; }; - // Get sidebar collapsed state from context (provided by LayoutShell) - const sidebarContext = useSidebarContextSafe(); - const isCollapsed = sidebarContext?.isCollapsed ?? false; - - // Calculate the left position for the inbox panel (relative to sidebar) - // Use dynamic width from context (tracks resize) for expanded state - const sidebarWidth = isCollapsed ? SIDEBAR_COLLAPSED_WIDTH : (sidebarContext?.sidebarWidth ?? 240); - if (!mounted) return null; // Shared content component for both docked and floating modes @@ -1126,49 +1114,8 @@ export function InboxSidebar({ // FLOATING MODE: Render with animation and click-away layer return ( - - {open && ( - <> - {/* Click-away layer - only covers the content area, not the sidebar */} - onOpenChange(false)} - aria-hidden="true" - /> - - {/* Clip container - positioned at sidebar edge with overflow hidden */} -
- - {inboxContent} - -
- - )} -
+ + {inboxContent} + ); } diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx new file mode 100644 index 000000000..9d74c01f1 --- /dev/null +++ b/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { AnimatePresence, motion } from "motion/react"; +import { cn } from "@/lib/utils"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { useSidebarContextSafe } from "../../hooks"; + +const SIDEBAR_COLLAPSED_WIDTH = 60; + +interface SidebarSlideOutPanelProps { + open: boolean; + onOpenChange: (open: boolean) => void; + ariaLabel: string; + width?: number; + children: React.ReactNode; +} + +/** + * Reusable slide-out panel that appears from the right edge of the sidebar. + * Used by InboxSidebar (floating mode), AllSharedChatsSidebar, and AllPrivateChatsSidebar. + * + * Must be rendered inside a positioned container (the LayoutShell's relative flex container) + * and within the SidebarProvider context. + */ +export function SidebarSlideOutPanel({ + open, + onOpenChange, + ariaLabel, + width = 360, + children, +}: SidebarSlideOutPanelProps) { + const isMobile = !useMediaQuery("(min-width: 640px)"); + const sidebarContext = useSidebarContextSafe(); + const isCollapsed = sidebarContext?.isCollapsed ?? false; + const sidebarWidth = isCollapsed + ? SIDEBAR_COLLAPSED_WIDTH + : (sidebarContext?.sidebarWidth ?? 240); + + return ( + + {open && ( + <> + {/* Click-away layer - covers the full container including the sidebar */} + onOpenChange(false)} + aria-hidden="true" + /> + + {/* Clip container - positioned at sidebar edge with overflow hidden */} +
+ + {children} + +
+ + )} +
+ ); +} From 123d3bccf42eef36098036f26940449f4d4cc011 Mon Sep 17 00:00:00 2001 From: Eric Lammertsma Date: Mon, 9 Feb 2026 12:05:19 -0500 Subject: [PATCH 4/5] Created more relatable chat input suggestions --- surfsense_web/components/assistant-ui/thread.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index dc30cfe40..9f3d45866 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -68,10 +68,10 @@ import { cn } from "@/lib/utils"; /** Placeholder texts that cycle in new chats when input is empty */ const CYCLING_PLACEHOLDERS = [ "Ask SurfSense anything or @mention docs.", - "Generate a podcast from marketing tips in the company handbook.", - "Sum up our vacation policy from Drive.", + "Generate a podcast from my vacation ideas in Notion.", + "Sum up last week's meeting notes from Drive in a bulleted list.", "Give me a brief overview of the most urgent tickets in Jira and Linear.", - "Create a concise table of today's top ten emails and calendar events.", + "Briefly, what are today's top ten important emails and calendar events?", "Check if this week's Slack messages reference any GitHub issues.", ]; From 9354f1c56a2d20f99cb7bbc1b3aecccf9815f55d Mon Sep 17 00:00:00 2001 From: Eric Lammertsma Date: Mon, 9 Feb 2026 16:09:57 -0500 Subject: [PATCH 5/5] Updated hero section with superpowers A/B and description --- .../components/homepage/hero-section.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index f30e7b9d7..e5b17d9e4 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -34,8 +34,8 @@ const GoogleLogo = ({ className }: { className?: string }) => ( export function HeroSection() { const containerRef = useRef(null); const parentRef = useRef(null); - const heroVariant = useFeatureFlagVariantKey("notebooklm_flag"); - const isNotebookLMVariant = heroVariant === "notebooklm"; + const heroVariant = useFeatureFlagVariantKey("notebooklm_superpowers_flag"); + const isNotebookLMVariant = heroVariant === "superpowers"; return (
- {isNotebookLMVariant ? ( -
-
- NotebookLM for Teams -
+ {isNotebookLMVariant ? ( +
+
+ NotebookLM with Superpowers
- ) : ( - <> - The AI Workspace{" "} -
-
- Built for Teams -
-
- - )} +
+ ) : ( +
+
+ NotebookLM for Teams +
+
+ )} {/* // TODO:aCTUAL DESCRITION */}

- Connect any LLM to your internal knowledge sources and chat with it in real time alongside - your team. + Connect any AI to your documents and knowledge sources. +

+

+ Then chat with it in real-time, even alongside your team.