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

@ -9,7 +9,7 @@ export const ThreadScrollToBottom: FC = () => {
<TooltipIconButton
tooltip="Scroll to bottom"
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 />
</TooltipIconButton>

View file

@ -19,6 +19,7 @@ import {
ChevronRightIcon,
CopyIcon,
DownloadIcon,
Globe,
Plus,
RefreshCwIcon,
Settings2,
@ -27,13 +28,13 @@ import {
Upload,
X,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useParams } from "next/navigation";
import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import {
agentToolsAtom,
disabledToolsAtom,
enabledToolCountAtom,
hydrateDisabledToolsAtom,
toggleToolAtom,
} from "@/atoms/agent-tools/agent-tools.atoms";
@ -118,7 +119,7 @@ export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) =>
const ThreadContent: FC = () => {
return (
<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={{
["--thread-max-width" as string]: "44rem",
}}
@ -140,7 +141,7 @@ const ThreadContent: FC = () => {
/>
<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))" }}
>
<ThreadScrollToBottom />
@ -161,7 +162,7 @@ const ThreadScrollToBottom: FC = () => {
<TooltipIconButton
tooltip="Scroll to bottom"
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 />
</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">
{/* 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">
<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>
{/* 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">
@ -589,7 +590,17 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
const disabledTools = useAtomValue(disabledToolsAtom);
const toggleTool = useSetAtom(toggleToolAtom);
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(() => {
hydrateDisabled();
@ -642,11 +653,11 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
<div className="flex items-center justify-between px-4 py-2">
<DrawerTitle className="text-sm font-medium">Agent Tools</DrawerTitle>
<span className="text-xs text-muted-foreground">
{enabledCount}/{agentTools?.length ?? 0} enabled
{filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
</span>
</div>
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
{agentTools?.map((tool) => {
{filteredTools?.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
return (
<div
@ -664,7 +675,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</div>
);
})}
{!agentTools?.length && (
{!filteredTools?.length && (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
Loading tools...
</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">
<span className="text-xs sm:text-sm font-medium">Agent Tools</span>
<span className="text-[10px] sm:text-xs text-muted-foreground">
{enabledCount}/{agentTools?.length ?? 0} enabled
{filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
</span>
</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"})`,
}}
>
{agentTools?.map((tool) => {
{filteredTools?.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
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">
@ -741,7 +752,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</Tooltip>
);
})}
{!agentTools?.length && (
{!filteredTools?.length && (
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
Loading tools...
</div>
@ -750,6 +761,39 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</PopoverContent>
</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 && (
<button
type="button"

View file

@ -54,7 +54,7 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
const showExpandButton = !isMobile && collapsed && hasRightPanelContent;
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 */}
<div className="flex flex-1 items-center gap-2 min-w-0">
{mobileMenuTrigger}

View file

@ -103,7 +103,7 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
return (
<aside
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">
{effectiveTab === "sources" && documentsOpen && documentsPanel && (

View file

@ -160,7 +160,7 @@ export function LayoutShell({
return (
<SidebarProvider value={sidebarContextValue}>
<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
mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />}
/>
@ -256,6 +256,12 @@ export function LayoutShell({
);
}
const anySlideOutOpen =
inbox?.isOpen ||
announcementsPanel?.open ||
allSharedChatsPanel?.open ||
allPrivateChatsPanel?.open;
// Desktop layout
return (
<SidebarProvider value={sidebarContextValue}>
@ -274,8 +280,15 @@ export function LayoutShell({
/>
</div>
{/* Main container with sidebar and content - relative for inbox positioning */}
<div className="relative flex flex-1 rounded-xl border bg-background overflow-hidden">
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content */}
<div
className={cn(
"relative hidden md:flex shrink-0 border bg-sidebar z-20 transition-[border-radius,border-color] duration-200",
anySlideOutOpen
? "rounded-l-xl border-r-0 delay-0"
: "rounded-xl delay-150"
)}
>
<Sidebar
searchSpace={searchSpace}
isCollapsed={isCollapsed}
@ -300,32 +313,16 @@ export function LayoutShell({
pageUsage={pageUsage}
theme={theme}
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}
sidebarWidth={sidebarWidth}
onResizeMouseDown={onResizeMouseDown}
isResizing={isResizing}
/>
<main className="flex-1 flex flex-col min-w-0">
<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 */}
{/* Slide-out panels render as siblings next to the sidebar */}
{inbox && (
<InboxSidebar
open={inbox.isOpen}
@ -336,7 +333,6 @@ export function LayoutShell({
/>
)}
{/* Announcements Sidebar */}
{announcementsPanel && (
<AnnouncementsSidebar
open={announcementsPanel.open}
@ -344,7 +340,6 @@ export function LayoutShell({
/>
)}
{/* All Shared Chats - slide-out panel */}
{allSharedChatsPanel && (
<AllSharedChatsSidebar
open={allSharedChatsPanel.open}
@ -353,7 +348,6 @@ export function LayoutShell({
/>
)}
{/* All Private Chats - slide-out panel */}
{allPrivateChatsPanel && (
<AllPrivateChatsSidebar
open={allPrivateChatsPanel.open}
@ -362,6 +356,37 @@ export function LayoutShell({
/>
)}
</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>
</TooltipProvider>
</SidebarProvider>

View file

@ -856,9 +856,9 @@ export function InboxSidebar({
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
<TabsTrigger
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" />
<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">
@ -868,9 +868,9 @@ export function InboxSidebar({
</TabsTrigger>
<TabsTrigger
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" />
<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">

View file

@ -86,7 +86,6 @@ export function Sidebar({
isLoadingChats = false,
disableTooltips = false,
sidebarWidth = SIDEBAR_MIN_WIDTH,
onResizeMouseDown,
isResizing = false,
}: SidebarProps) {
const t = useTranslations("sidebar");
@ -102,19 +101,6 @@ export function Sidebar({
)}
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 */}
{isCollapsed ? (
<div className="flex h-14 shrink-0 items-center justify-center border-b">

View file

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

View file

@ -233,11 +233,11 @@ export function ModelSelector({
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
variant="ghost"
size="sm"
role="combobox"
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 ? (
<>
@ -282,10 +282,7 @@ export function ModelSelector({
</>
)}
<ChevronDown
className={cn(
"h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0 transition-transform duration-200",
open && "rotate-180"
)}
className="h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0"
/>
</Button>
</PopoverTrigger>