feat: update UI components to utilize new main panel styling

- Replaced background styles with `bg-main-panel` in various components for consistent theming.
- Enhanced the `Header`, `LayoutShell`, and `Thread` components to improve visual coherence.
- Adjusted tool management UI to reflect new design standards, ensuring a unified user experience.
This commit is contained in:
Anish Sarkar 2026-03-17 01:09:15 +05:30
parent b5328a267f
commit 993c8539e8
12 changed files with 187 additions and 112 deletions

View file

@ -566,7 +566,7 @@ DISABLED TOOLS (by user):
The following tools are available in SurfSense but have been disabled by the user for this session: {disabled_list}. The following tools are available in SurfSense but have been disabled by the user for this session: {disabled_list}.
You do NOT have access to these tools and MUST NOT claim you can use them. You do NOT have access to these tools and MUST NOT claim you can use them.
If the user asks about a capability provided by a disabled tool, let them know the relevant tool If the user asks about a capability provided by a disabled tool, let them know the relevant tool
is currently disabled and they can re-enable it from the tools menu (wrench icon) in the composer toolbar. is currently disabled and they can re-enable it.
""") """)
parts.append("\n</tools>\n") parts.append("\n</tools>\n")

View file

@ -1596,7 +1596,7 @@ export default function NewChatPage() {
// Show loading state only when loading an existing thread // Show loading state only when loading an existing thread
if (isInitializing) { if (isInitializing) {
return ( return (
<div className="flex h-[calc(100dvh-64px)] flex-col bg-background px-4"> <div className="flex h-[calc(100dvh-64px)] flex-col bg-main-panel px-4">
<div className="mx-auto w-full max-w-[44rem] flex flex-1 flex-col gap-6 py-8"> <div className="mx-auto w-full max-w-[44rem] flex flex-1 flex-col gap-6 py-8">
{/* User message */} {/* User message */}
<div className="flex justify-end"> <div className="flex justify-end">
@ -1624,7 +1624,7 @@ export default function NewChatPage() {
</div> </div>
{/* Input bar */} {/* Input bar */}
<div className="sticky bottom-0 pb-6 bg-background"> <div className="sticky bottom-0 pb-6 bg-main-panel">
<div className="mx-auto w-full max-w-[44rem]"> <div className="mx-auto w-full max-w-[44rem]">
<Skeleton className="h-24 w-full rounded-2xl" /> <Skeleton className="h-24 w-full rounded-2xl" />
</div> </div>

View file

@ -47,6 +47,7 @@
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
--main-panel: oklch(1 0 0);
--syntax-bg: #f5f5f5; --syntax-bg: #f5f5f5;
--brand: oklch(0.623 0.214 259.815); --brand: oklch(0.623 0.214 259.815);
--highlight: oklch(0.852 0.199 91.936); --highlight: oklch(0.852 0.199 91.936);
@ -85,6 +86,7 @@
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0); --sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0); --sidebar-ring: oklch(0.439 0 0);
--main-panel: oklch(0.18 0 0);
--syntax-bg: #1e1e1e; --syntax-bg: #1e1e1e;
--brand: oklch(0.707 0.165 254.624); --brand: oklch(0.707 0.165 254.624);
--highlight: oklch(0.852 0.199 91.936); --highlight: oklch(0.852 0.199 91.936);
@ -115,6 +117,7 @@
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
--color-main-panel: var(--main-panel);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);

View file

@ -9,7 +9,7 @@ export const ThreadScrollToBottom: FC = () => {
<TooltipIconButton <TooltipIconButton
tooltip="Scroll to bottom" tooltip="Scroll to bottom"
variant="outline" variant="outline"
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent" className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
> >
<ArrowDownIcon /> <ArrowDownIcon />
</TooltipIconButton> </TooltipIconButton>

View file

@ -19,6 +19,7 @@ import {
ChevronRightIcon, ChevronRightIcon,
CopyIcon, CopyIcon,
DownloadIcon, DownloadIcon,
Globe,
Plus, Plus,
RefreshCwIcon, RefreshCwIcon,
Settings2, Settings2,
@ -27,13 +28,13 @@ import {
Upload, Upload,
X, X,
} from "lucide-react"; } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { import {
agentToolsAtom, agentToolsAtom,
disabledToolsAtom, disabledToolsAtom,
enabledToolCountAtom,
hydrateDisabledToolsAtom, hydrateDisabledToolsAtom,
toggleToolAtom, toggleToolAtom,
} from "@/atoms/agent-tools/agent-tools.atoms"; } from "@/atoms/agent-tools/agent-tools.atoms";
@ -118,7 +119,7 @@ export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) =>
const ThreadContent: FC = () => { const ThreadContent: FC = () => {
return ( return (
<ThreadPrimitive.Root <ThreadPrimitive.Root
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-background" className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-main-panel"
style={{ style={{
["--thread-max-width" as string]: "44rem", ["--thread-max-width" as string]: "44rem",
}} }}
@ -140,7 +141,7 @@ const ThreadContent: FC = () => {
/> />
<ThreadPrimitive.ViewportFooter <ThreadPrimitive.ViewportFooter
className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6" className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-main-panel pb-4 md:pb-6"
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }} style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
> >
<ThreadScrollToBottom /> <ThreadScrollToBottom />
@ -161,7 +162,7 @@ const ThreadScrollToBottom: FC = () => {
<TooltipIconButton <TooltipIconButton
tooltip="Scroll to bottom" tooltip="Scroll to bottom"
variant="outline" variant="outline"
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent" className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
> >
<ArrowDownIcon /> <ArrowDownIcon />
</TooltipIconButton> </TooltipIconButton>
@ -232,7 +233,7 @@ const ThreadWelcome: FC = () => {
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative"> <div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
{/* Greeting positioned above the composer */} {/* Greeting positioned above the composer */}
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center"> <div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
<h1 className="aui-thread-welcome-message-inner text-3xl md:text-5xl">{greeting}</h1> <h1 className="aui-thread-welcome-message-inner text-3xl md:text-5xl select-none">{greeting}</h1>
</div> </div>
{/* Composer - top edge fixed, expands downward only */} {/* Composer - top edge fixed, expands downward only */}
<div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0"> <div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0">
@ -589,7 +590,17 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
const disabledTools = useAtomValue(disabledToolsAtom); const disabledTools = useAtomValue(disabledToolsAtom);
const toggleTool = useSetAtom(toggleToolAtom); const toggleTool = useSetAtom(toggleToolAtom);
const hydrateDisabled = useSetAtom(hydrateDisabledToolsAtom); const hydrateDisabled = useSetAtom(hydrateDisabledToolsAtom);
const enabledCount = useAtomValue(enabledToolCountAtom);
const hasWebSearchTool = agentTools?.some((t) => t.name === "web_search") ?? false;
const isWebSearchEnabled = hasWebSearchTool && !disabledTools.includes("web_search");
const filteredTools = useMemo(
() => agentTools?.filter((t) => t.name !== "web_search"),
[agentTools]
);
const filteredEnabledCount = useMemo(() => {
if (!filteredTools) return 0;
return filteredTools.length - disabledTools.filter((d) => filteredTools.some((t) => t.name === d)).length;
}, [filteredTools, disabledTools]);
useEffect(() => { useEffect(() => {
hydrateDisabled(); hydrateDisabled();
@ -642,11 +653,11 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
<div className="flex items-center justify-between px-4 py-2"> <div className="flex items-center justify-between px-4 py-2">
<DrawerTitle className="text-sm font-medium">Agent Tools</DrawerTitle> <DrawerTitle className="text-sm font-medium">Agent Tools</DrawerTitle>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{enabledCount}/{agentTools?.length ?? 0} enabled {filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
</span> </span>
</div> </div>
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}> <div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
{agentTools?.map((tool) => { {filteredTools?.map((tool) => {
const isDisabled = disabledTools.includes(tool.name); const isDisabled = disabledTools.includes(tool.name);
return ( return (
<div <div
@ -664,7 +675,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</div> </div>
); );
})} })}
{!agentTools?.length && ( {!filteredTools?.length && (
<div className="px-4 py-6 text-center text-sm text-muted-foreground"> <div className="px-4 py-6 text-center text-sm text-muted-foreground">
Loading tools... Loading tools...
</div> </div>
@ -707,7 +718,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
<div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b"> <div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b">
<span className="text-xs sm:text-sm font-medium">Agent Tools</span> <span className="text-xs sm:text-sm font-medium">Agent Tools</span>
<span className="text-[10px] sm:text-xs text-muted-foreground"> <span className="text-[10px] sm:text-xs text-muted-foreground">
{enabledCount}/{agentTools?.length ?? 0} enabled {filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
</span> </span>
</div> </div>
<div <div
@ -718,7 +729,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`, WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
}} }}
> >
{agentTools?.map((tool) => { {filteredTools?.map((tool) => {
const isDisabled = disabledTools.includes(tool.name); const isDisabled = disabledTools.includes(tool.name);
const row = ( const row = (
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors"> <div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
@ -741,7 +752,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</Tooltip> </Tooltip>
); );
})} })}
{!agentTools?.length && ( {!filteredTools?.length && (
<div className="px-3 py-4 text-center text-xs text-muted-foreground"> <div className="px-3 py-4 text-center text-xs text-muted-foreground">
Loading tools... Loading tools...
</div> </div>
@ -750,6 +761,39 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)} )}
{hasWebSearchTool && (
<button
type="button"
onClick={() => toggleTool("web_search")}
className={cn(
"rounded-full transition-all flex items-center gap-1 px-2 py-1 border h-8",
isWebSearchEnabled
? "bg-sky-500/15 border-sky-500/60 text-sky-500"
: "bg-transparent border-transparent text-muted-foreground hover:text-foreground"
)}
>
<motion.div
animate={{ rotate: isWebSearchEnabled ? 360 : 0, scale: isWebSearchEnabled ? 1.1 : 1 }}
whileHover={{ rotate: isWebSearchEnabled ? 360 : 15, scale: 1.1, transition: { type: "spring", stiffness: 300, damping: 10 } }}
transition={{ type: "spring", stiffness: 260, damping: 25 }}
>
<Globe className="size-4" />
</motion.div>
<AnimatePresence>
{isWebSearchEnabled && (
<motion.span
initial={{ width: 0, opacity: 0 }}
animate={{ width: "auto", opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="text-xs overflow-hidden whitespace-nowrap"
>
Search
</motion.span>
)}
</AnimatePresence>
</button>
)}
{sidebarDocs.length > 0 && ( {sidebarDocs.length > 0 && (
<button <button
type="button" type="button"

View file

@ -54,7 +54,7 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
const showExpandButton = !isMobile && collapsed && hasRightPanelContent; const showExpandButton = !isMobile && collapsed && hasRightPanelContent;
return ( return (
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4"> <header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-main-panel/95 backdrop-blur supports-backdrop-filter:bg-main-panel/60 px-4">
{/* Left side - Mobile menu trigger + Model selector */} {/* Left side - Mobile menu trigger + Model selector */}
<div className="flex flex-1 items-center gap-2 min-w-0"> <div className="flex flex-1 items-center gap-2 min-w-0">
{mobileMenuTrigger} {mobileMenuTrigger}

View file

@ -103,7 +103,7 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
return ( return (
<aside <aside
style={{ width: targetWidth }} style={{ width: targetWidth }}
className="flex h-full shrink-0 flex-col border-l bg-background overflow-hidden transition-[width] duration-200 ease-out" className="flex h-full shrink-0 flex-col rounded-xl border bg-background overflow-hidden transition-[width] duration-200 ease-out"
> >
<div className="relative flex-1 min-h-0 overflow-hidden"> <div className="relative flex-1 min-h-0 overflow-hidden">
{effectiveTab === "sources" && documentsOpen && documentsPanel && ( {effectiveTab === "sources" && documentsOpen && documentsPanel && (

View file

@ -160,7 +160,7 @@ export function LayoutShell({
return ( return (
<SidebarProvider value={sidebarContextValue}> <SidebarProvider value={sidebarContextValue}>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<div className={cn("flex h-screen w-full flex-col bg-background", className)}> <div className={cn("flex h-screen w-full flex-col bg-main-panel", className)}>
<Header <Header
mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />} mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />}
/> />
@ -256,6 +256,12 @@ export function LayoutShell({
); );
} }
const anySlideOutOpen =
inbox?.isOpen ||
announcementsPanel?.open ||
allSharedChatsPanel?.open ||
allPrivateChatsPanel?.open;
// Desktop layout // Desktop layout
return ( return (
<SidebarProvider value={sidebarContextValue}> <SidebarProvider value={sidebarContextValue}>
@ -274,8 +280,15 @@ export function LayoutShell({
/> />
</div> </div>
{/* Main container with sidebar and content - relative for inbox positioning */} {/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content */}
<div className="relative flex flex-1 rounded-xl border bg-background overflow-hidden"> <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"
)}
>
<Sidebar <Sidebar
searchSpace={searchSpace} searchSpace={searchSpace}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
@ -300,32 +313,16 @@ export function LayoutShell({
pageUsage={pageUsage} pageUsage={pageUsage}
theme={theme} theme={theme}
setTheme={setTheme} setTheme={setTheme}
className="hidden md:flex border-r shrink-0" className={cn(
"flex shrink-0 transition-[border-radius] duration-200",
anySlideOutOpen ? "rounded-l-xl delay-0" : "rounded-xl delay-150"
)}
isLoadingChats={isLoadingChats} isLoadingChats={isLoadingChats}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
onResizeMouseDown={onResizeMouseDown}
isResizing={isResizing} isResizing={isResizing}
/> />
<main className="flex-1 flex flex-col min-w-0"> {/* Slide-out panels render as siblings next to the sidebar */}
<Header />
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
{children}
</div>
</main>
{/* Right panel — tabbed Sources/Report (desktop only) */}
{documentsPanel && (
<RightPanel
documentsPanel={{
open: documentsPanel.open,
onOpenChange: documentsPanel.onOpenChange,
}}
/>
)}
{/* Inbox Sidebar - slide-out panel */}
{inbox && ( {inbox && (
<InboxSidebar <InboxSidebar
open={inbox.isOpen} open={inbox.isOpen}
@ -336,7 +333,6 @@ export function LayoutShell({
/> />
)} )}
{/* Announcements Sidebar */}
{announcementsPanel && ( {announcementsPanel && (
<AnnouncementsSidebar <AnnouncementsSidebar
open={announcementsPanel.open} open={announcementsPanel.open}
@ -344,7 +340,6 @@ export function LayoutShell({
/> />
)} )}
{/* All Shared Chats - slide-out panel */}
{allSharedChatsPanel && ( {allSharedChatsPanel && (
<AllSharedChatsSidebar <AllSharedChatsSidebar
open={allSharedChatsPanel.open} open={allSharedChatsPanel.open}
@ -353,7 +348,6 @@ export function LayoutShell({
/> />
)} )}
{/* All Private Chats - slide-out panel */}
{allPrivateChatsPanel && ( {allPrivateChatsPanel && (
<AllPrivateChatsSidebar <AllPrivateChatsSidebar
open={allPrivateChatsPanel.open} open={allPrivateChatsPanel.open}
@ -362,6 +356,37 @@ export function LayoutShell({
/> />
)} )}
</div> </div>
{/* Resize handle — negative margins eat the flex gap so spacing stays unchanged */}
{!isCollapsed && (
<div
role="slider"
aria-label="Resize sidebar"
tabIndex={0}
onMouseDown={onResizeMouseDown}
className="hidden md:block h-full cursor-col-resize z-30"
style={{ width: 8, marginLeft: -8, marginRight: -8 }}
/>
)}
{/* Main content panel */}
<div className="relative flex flex-1 flex-col rounded-xl border bg-main-panel overflow-hidden min-w-0">
<Header />
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
{children}
</div>
</div>
{/* Right panel — tabbed Sources/Report (desktop only) */}
{documentsPanel && (
<RightPanel
documentsPanel={{
open: documentsPanel.open,
onOpenChange: documentsPanel.onOpenChange,
}}
/>
)}
</div> </div>
</TooltipProvider> </TooltipProvider>
</SidebarProvider> </SidebarProvider>

View file

@ -856,9 +856,9 @@ export function InboxSidebar({
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b"> <TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
<TabsTrigger <TabsTrigger
value="comments" value="comments"
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none" className="group flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
> >
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors"> <span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted group-data-[state=active]:bg-muted transition-colors">
<MessageSquare className="h-4 w-4" /> <MessageSquare className="h-4 w-4" />
<span>{t("comments") || "Comments"}</span> <span>{t("comments") || "Comments"}</span>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium"> <span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
@ -868,9 +868,9 @@ export function InboxSidebar({
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="status" value="status"
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none" className="group flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
> >
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors"> <span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted group-data-[state=active]:bg-muted transition-colors">
<History className="h-4 w-4" /> <History className="h-4 w-4" />
<span>{t("status") || "Status"}</span> <span>{t("status") || "Status"}</span>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium"> <span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">

View file

@ -86,7 +86,6 @@ export function Sidebar({
isLoadingChats = false, isLoadingChats = false,
disableTooltips = false, disableTooltips = false,
sidebarWidth = SIDEBAR_MIN_WIDTH, sidebarWidth = SIDEBAR_MIN_WIDTH,
onResizeMouseDown,
isResizing = false, isResizing = false,
}: SidebarProps) { }: SidebarProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
@ -102,19 +101,6 @@ export function Sidebar({
)} )}
style={!isCollapsed ? { width: sidebarWidth } : undefined} style={!isCollapsed ? { width: sidebarWidth } : undefined}
> >
{/* Resize handle on right border */}
{!isCollapsed && onResizeMouseDown && (
<div
role="slider"
aria-label="Resize sidebar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={50}
tabIndex={0}
onMouseDown={onResizeMouseDown}
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10"
/>
)}
{/* Header - search space name or collapse button when collapsed */} {/* Header - search space name or collapse button when collapsed */}
{isCollapsed ? ( {isCollapsed ? (
<div className="flex h-14 shrink-0 items-center justify-center border-b"> <div className="flex h-14 shrink-0 items-center justify-center border-b">

View file

@ -1,15 +1,11 @@
"use client"; "use client";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useMediaQuery } from "@/hooks/use-media-query"; import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
import { useSidebarContextSafe } from "../../hooks";
export const SLIDEOUT_PANEL_OPENED_EVENT = "slideout-panel-opened"; export const SLIDEOUT_PANEL_OPENED_EVENT = "slideout-panel-opened";
const SIDEBAR_COLLAPSED_WIDTH = 60;
interface SidebarSlideOutPanelProps { interface SidebarSlideOutPanelProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -19,11 +15,12 @@ interface SidebarSlideOutPanelProps {
} }
/** /**
* Reusable slide-out panel that appears from the right edge of the sidebar. * Reusable slide-out panel that extends from the sidebar.
* Used by InboxSidebar (floating mode), AllSharedChatsSidebar, and AllPrivateChatsSidebar.
* *
* Must be rendered inside a positioned container (the LayoutShell's relative flex container) * Desktop: absolutely positioned at the sidebar's right edge, overlaying the main
* and within the SidebarProvider context. * content with a blur backdrop. Does not push/shrink the main content.
*
* Mobile: full-width absolute overlay (unchanged).
*/ */
export function SidebarSlideOutPanel({ export function SidebarSlideOutPanel({
open, open,
@ -33,11 +30,6 @@ export function SidebarSlideOutPanel({
children, children,
}: SidebarSlideOutPanelProps) { }: SidebarSlideOutPanelProps) {
const isMobile = !useMediaQuery("(min-width: 640px)"); const isMobile = !useMediaQuery("(min-width: 640px)");
const sidebarContext = useSidebarContextSafe();
const isCollapsed = sidebarContext?.isCollapsed ?? false;
const sidebarWidth = isCollapsed
? SIDEBAR_COLLAPSED_WIDTH
: (sidebarContext?.sidebarWidth ?? 240);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -45,41 +37,30 @@ export function SidebarSlideOutPanel({
} }
}, [open]); }, [open]);
return ( const handleEscape = useCallback(
<AnimatePresence> (e: KeyboardEvent) => {
{open && ( if (e.key === "Escape") onOpenChange(false);
<> },
{/* Backdrop overlay with blur — desktop only, covers main content area (right of sidebar) */} [onOpenChange]
{!isMobile && ( );
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
style={{ left: sidebarWidth }}
className="absolute inset-y-0 right-0 z-20 bg-black/30 backdrop-blur-sm"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
)}
{/* Clip container - positioned at sidebar edge with overflow hidden */} useEffect(() => {
<div if (!open) return;
style={{ document.addEventListener("keydown", handleEscape);
left: isMobile ? 0 : sidebarWidth, return () => document.removeEventListener("keydown", handleEscape);
width: isMobile ? "100%" : width, }, [open, handleEscape]);
}}
className={cn("absolute z-30 overflow-hidden pointer-events-none", "inset-y-0")} if (isMobile) {
> return (
<AnimatePresence>
{open && (
<div className="absolute left-0 inset-y-0 z-30 w-full overflow-hidden pointer-events-none">
<motion.div <motion.div
initial={{ x: "-100%" }} initial={{ x: "-100%" }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: "-100%" }} exit={{ x: "-100%" }}
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }} transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
className={cn( className="h-full w-full bg-sidebar text-sidebar-foreground flex flex-col pointer-events-auto select-none"
"h-full w-full bg-sidebar text-sidebar-foreground flex flex-col pointer-events-auto select-none",
"sm:border-r sm:shadow-xl"
)}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={ariaLabel} aria-label={ariaLabel}
@ -87,6 +68,45 @@ export function SidebarSlideOutPanel({
{children} {children}
</motion.div> </motion.div>
</div> </div>
)}
</AnimatePresence>
);
}
return (
<AnimatePresence initial={false}>
{open && (
<>
{/* Blur backdrop covering the main content area (right of sidebar) */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="absolute z-10 bg-black/30 backdrop-blur-sm rounded-xl"
style={{ top: -9, bottom: -9, left: "calc(100% + 1px)", width: "200vw" }}
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
{/* Panel extending from sidebar's right edge, flush with the wrapper border */}
<motion.div
initial={{ width: 0 }}
animate={{ width }}
exit={{ width: 0 }}
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
className="absolute z-20 overflow-hidden"
style={{ left: "100%", top: -1, bottom: -1 }}
>
<div
style={{ width }}
className="h-full bg-sidebar text-sidebar-foreground flex flex-col select-none border rounded-r-xl shadow-xl"
role="dialog"
aria-label={ariaLabel}
>
{children}
</div>
</motion.div>
</> </>
)} )}
</AnimatePresence> </AnimatePresence>

View file

@ -233,11 +233,11 @@ export function ModelSelector({
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="ghost"
size="sm" size="sm"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn("h-8 gap-2 px-3 text-sm border-border/60 select-none", className)} className={cn("h-8 gap-2 px-3 text-sm bg-main-panel hover:bg-accent/50 dark:hover:bg-white/[0.06] border border-border/40 select-none", className)}
> >
{isLoading ? ( {isLoading ? (
<> <>
@ -282,10 +282,7 @@ export function ModelSelector({
</> </>
)} )}
<ChevronDown <ChevronDown
className={cn( className="h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0"
"h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0 transition-transform duration-200",
open && "rotate-180"
)}
/> />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>