refactor(header, layout): simplify header component by removing unused tabs and mobile checks; enhance RightPanel and LayoutShell with improved styling and sidebar functionality

This commit is contained in:
Anish Sarkar 2026-04-29 03:46:40 +05:30
parent 360e21eee4
commit 65f3916fc3
6 changed files with 59 additions and 44 deletions

View file

@ -4,12 +4,10 @@ import { useAtomValue } from "jotai";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom"; import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { activeTabAtom, tabsAtom } from "@/atoms/tabs/tabs.atom"; import { activeTabAtom } from "@/atoms/tabs/tabs.atom";
import { ChatHeader } from "@/components/new-chat/chat-header"; import { ChatHeader } from "@/components/new-chat/chat-header";
import { ChatShareButton } from "@/components/new-chat/chat-share-button"; import { ChatShareButton } from "@/components/new-chat/chat-share-button";
import { useIsMobile } from "@/hooks/use-mobile";
import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence"; import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence";
import { RightPanelExpandButton } from "../right-panel/RightPanel";
interface HeaderProps { interface HeaderProps {
mobileMenuTrigger?: React.ReactNode; mobileMenuTrigger?: React.ReactNode;
@ -18,14 +16,11 @@ interface HeaderProps {
export function Header({ mobileMenuTrigger }: HeaderProps) { export function Header({ mobileMenuTrigger }: HeaderProps) {
const pathname = usePathname(); const pathname = usePathname();
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const isMobile = useIsMobile();
const activeTab = useAtomValue(activeTabAtom); const activeTab = useAtomValue(activeTabAtom);
const tabs = useAtomValue(tabsAtom);
const isFreePage = pathname?.startsWith("/free") ?? false; const isFreePage = pathname?.startsWith("/free") ?? false;
const isChatPage = pathname?.includes("/new-chat") ?? false; const isChatPage = pathname?.includes("/new-chat") ?? false;
const isDocumentTab = activeTab?.type === "document"; const isDocumentTab = activeTab?.type === "document";
const hasTabBar = tabs.length > 1;
const currentThreadState = useAtomValue(currentThreadAtom); const currentThreadState = useAtomValue(currentThreadAtom);
@ -72,7 +67,6 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
{hasThread && ( {hasThread && (
<ChatShareButton thread={threadForButton} onVisibilityChange={handleVisibilityChange} /> <ChatShareButton thread={threadForButton} onVisibilityChange={handleVisibilityChange} />
)} )}
{!isMobile && !hasTabBar && <RightPanelExpandButton />}
</div> </div>
</header> </header>
); );

View file

@ -48,7 +48,12 @@ function CollapseButton({ onClick }: { onClick: () => void }) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={onClick} className="h-8 w-8 shrink-0"> <Button
variant="ghost"
size="icon"
onClick={onClick}
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted/40"
>
<PanelRight className="h-4 w-4" /> <PanelRight className="h-4 w-4" />
<span className="sr-only">Collapse panel</span> <span className="sr-only">Collapse panel</span>
</Button> </Button>
@ -88,7 +93,7 @@ export function RightPanelExpandButton() {
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => startTransition(() => setCollapsed(false))} onClick={() => startTransition(() => setCollapsed(false))}
className="h-8 w-8 shrink-0 -m-0.5" className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/40"
> >
<PanelRight className="h-4 w-4" /> <PanelRight className="h-4 w-4" />
<span className="sr-only">Expand panel</span> <span className="sr-only">Expand panel</span>
@ -161,7 +166,7 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
return ( return (
<aside <aside
style={{ width: targetWidth }} style={{ width: targetWidth }}
className="flex h-full shrink-0 flex-col rounded-xl border bg-sidebar text-sidebar-foreground overflow-hidden transition-[width] duration-200 ease-out" className="flex h-full shrink-0 flex-col border-l border-b bg-sidebar text-sidebar-foreground 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

@ -120,22 +120,45 @@ interface LayoutShellProps {
function MainContentPanel({ function MainContentPanel({
isChatPage, isChatPage,
isSidebarCollapsed,
onTabSwitch, onTabSwitch,
onNewChat, onNewChat,
leftActions, leftActions,
showResizeHandle = false,
onResizeMouseDown,
children, children,
}: { }: {
isChatPage: boolean; isChatPage: boolean;
isSidebarCollapsed: boolean;
onTabSwitch?: (tab: Tab) => void; onTabSwitch?: (tab: Tab) => void;
onNewChat?: () => void; onNewChat?: () => void;
leftActions?: React.ReactNode; leftActions?: React.ReactNode;
showResizeHandle?: boolean;
onResizeMouseDown?: (e: React.MouseEvent) => void;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const activeTab = useAtomValue(activeTabAtom); const activeTab = useAtomValue(activeTabAtom);
const isDocumentTab = activeTab?.type === "document"; const isDocumentTab = activeTab?.type === "document";
return ( return (
<div className="relative flex flex-1 flex-col min-w-0"> <div
className={cn(
"relative flex flex-1 flex-col min-w-0 -ml-2",
isSidebarCollapsed ? "" : "border-l border-border/60"
)}
>
{showResizeHandle && onResizeMouseDown && (
<div
role="slider"
aria-label="Resize sidebar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={50}
tabIndex={0}
onMouseDown={onResizeMouseDown}
className="absolute left-0 top-0 hidden md:block h-full w-2 -translate-x-1/2 cursor-col-resize z-30 focus:outline-none"
/>
)}
<TabBar <TabBar
onTabSwitch={onTabSwitch} onTabSwitch={onTabSwitch}
onNewChat={onNewChat} onNewChat={onNewChat}
@ -143,7 +166,7 @@ function MainContentPanel({
rightActions={<RightPanelExpandButton />} rightActions={<RightPanelExpandButton />}
className="min-w-0" className="min-w-0"
/> />
<div className="relative flex flex-1 flex-col rounded-xl border bg-main-panel overflow-hidden min-w-0"> <div className="relative flex flex-1 flex-col border border-l-0 border-r-0 border-t-0 bg-main-panel overflow-hidden min-w-0">
<Header /> <Header />
{isDocumentTab && activeTab.documentId && activeTab.searchSpaceId ? ( {isDocumentTab && activeTab.documentId && activeTab.searchSpaceId ? (
@ -515,26 +538,14 @@ export function LayoutShell({
</SidebarSlideOutPanel> </SidebarSlideOutPanel>
</div> </div>
{/* Resize handle — negative margins eat the flex gap so spacing stays unchanged */}
{!isCollapsed && (
<div
role="slider"
aria-label="Resize sidebar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={50}
tabIndex={0}
onMouseDown={onResizeMouseDown}
className="hidden md:block h-full cursor-col-resize z-30 focus:outline-none"
style={{ width: 8, marginLeft: -8, marginRight: -8 }}
/>
)}
{/* Main content panel */} {/* Main content panel */}
<MainContentPanel <MainContentPanel
isChatPage={isChatPage} isChatPage={isChatPage}
isSidebarCollapsed={isCollapsed}
onTabSwitch={onTabSwitch} onTabSwitch={onTabSwitch}
onNewChat={onNewChat} onNewChat={onNewChat}
showResizeHandle={!isCollapsed}
onResizeMouseDown={onResizeMouseDown}
leftActions={ leftActions={
isCollapsed ? ( isCollapsed ? (
<SidebarCollapseButton isCollapsed={isCollapsed} onToggle={toggleCollapsed} /> <SidebarCollapseButton isCollapsed={isCollapsed} onToggle={toggleCollapsed} />
@ -546,12 +557,14 @@ export function LayoutShell({
{/* Right panel — tabbed Sources/Report (desktop only) */} {/* Right panel — tabbed Sources/Report (desktop only) */}
{documentsPanel && ( {documentsPanel && (
<RightPanel <div className="-ml-2 -mr-2">
documentsPanel={{ <RightPanel
open: documentsPanel.open, documentsPanel={{
onOpenChange: documentsPanel.onOpenChange, open: documentsPanel.open,
}} onOpenChange: documentsPanel.onOpenChange,
/> }}
/>
</div>
)} )}
</div> </div>
</TooltipProvider> </TooltipProvider>

View file

@ -1207,7 +1207,7 @@ function AuthenticatedDocumentsSidebarBase({
const documentsContent = ( const documentsContent = (
<> <>
<div className="shrink-0 flex h-12 items-center px-4"> <div className="shrink-0 flex h-12 items-center px-3 border-b">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isMobile && ( {isMobile && (
@ -1692,7 +1692,7 @@ function AnonymousDocumentsSidebar({
/> />
{/* Header */} {/* Header */}
<div className="shrink-0 flex h-12 items-center px-4"> <div className="shrink-0 flex h-12 items-center px-3 border-b">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="select-none text-base font-semibold">{t("title") || "Documents"}</h2> <h2 className="select-none text-base font-semibold">{t("title") || "Documents"}</h2>

View file

@ -22,7 +22,12 @@ export function SidebarCollapseButton({
const { shortcutKeys } = usePlatformShortcut(); const { shortcutKeys } = usePlatformShortcut();
const button = ( const button = (
<Button variant="ghost" size="icon" onClick={onToggle} className="h-8 w-8 shrink-0"> <Button
variant="ghost"
size="icon"
onClick={onToggle}
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted/40"
>
<PanelLeft className="h-4 w-4" /> <PanelLeft className="h-4 w-4" />
<span className="sr-only">{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}</span> <span className="sr-only">{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}</span>
</Button> </Button>

View file

@ -77,21 +77,19 @@ export function TabBar({
// Keep action slots visible even with one/no tabs // Keep action slots visible even with one/no tabs
const hasAuxActions = !!leftActions || !!rightActions || !!onNewChat; const hasAuxActions = !!leftActions || !!rightActions || !!onNewChat;
const hasMultipleChats = tabs.length > 1;
if (tabs.length <= 1 && !hasAuxActions) return null; if (tabs.length <= 1 && !hasAuxActions) return null;
return ( return (
<div <div
className={cn( className={cn(
"mb-2 flex h-9 items-center shrink-0 px-1 gap-0.5 select-none", "mb-0 flex h-12 items-center shrink-0 px-1 gap-0.5 select-none border-b border-border/60",
hasMultipleChats && "mt-1",
className className
)} )}
> >
{leftActions ? <div className="flex items-center gap-0.5 shrink-0">{leftActions}</div> : null} {leftActions ? <div className="flex items-center gap-0.5 shrink-0">{leftActions}</div> : null}
<div <div
ref={scrollRef} ref={scrollRef}
className="flex h-full items-center flex-1 gap-0.5 overflow-x-auto overflow-y-hidden scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden py-1" className="flex h-8 items-center flex-1 gap-3 overflow-x-auto overflow-y-hidden scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden py-0"
> >
{tabs.map((tab) => { {tabs.map((tab) => {
const isActive = tab.id === activeTabId; const isActive = tab.id === activeTabId;
@ -103,7 +101,7 @@ export function TabBar({
data-tab-id={tab.id} data-tab-id={tab.id}
onClick={() => handleTabClick(tab)} onClick={() => handleTabClick(tab)}
className={cn( className={cn(
"group relative flex h-full w-[150px] items-center px-3 min-h-0 overflow-hidden text-[13px] font-medium rounded-lg transition-all duration-150 shrink-0", "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",
isActive isActive
? "bg-muted/60 text-foreground" ? "bg-muted/60 text-foreground"
: "bg-transparent text-muted-foreground hover:bg-muted/30 hover:text-foreground" : "bg-transparent text-muted-foreground hover:bg-muted/30 hover:text-foreground"
@ -136,15 +134,15 @@ export function TabBar({
); );
})} })}
</div> </div>
<div className="flex items-center gap-0.5 shrink-0"> <div className="flex items-center gap-0.5 shrink-0 pr-2">
{onNewChat && ( {onNewChat && (
<button <button
type="button" type="button"
onClick={onNewChat} onClick={onNewChat}
className="flex h-6 w-6 items-center justify-center shrink-0 rounded-md text-muted-foreground transition-all duration-150 hover:text-foreground hover:bg-muted/40" className="flex h-8 w-8 items-center justify-center shrink-0 rounded-md text-muted-foreground transition-all duration-150 hover:text-foreground hover:bg-muted/40"
title="New Chat" title="New Chat"
> >
<Plus className="size-3.5" /> <Plus className="size-4" />
</button> </button>
)} )}
{rightActions} {rightActions}