feat: collapsible chat sidebar with top bar mode
Sidebar collapses to a horizontal top bar with "New" and "History" buttons plus active chat title. Persists to localStorage. Fixes layout nesting so TaskLane renders correctly when sidebar is collapsed. Also removes chat input separator line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Plus, Pin, Trash2, MessageSquare } from 'lucide-react'
|
||||
import { Plus, Pin, Trash2, MessageSquare, History, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ChatListItem } from '@/types/assistant-chat'
|
||||
|
||||
@@ -11,6 +11,8 @@ interface ChatSidebarProps {
|
||||
onTogglePin: (id: string, pinned: boolean) => void
|
||||
mobileOpen?: boolean
|
||||
onMobileClose?: () => void
|
||||
collapsed?: boolean
|
||||
onToggleCollapse?: () => void
|
||||
}
|
||||
|
||||
export function ChatSidebar({
|
||||
@@ -22,6 +24,8 @@ export function ChatSidebar({
|
||||
onTogglePin,
|
||||
mobileOpen = false,
|
||||
onMobileClose,
|
||||
collapsed = false,
|
||||
onToggleCollapse,
|
||||
}: ChatSidebarProps) {
|
||||
const pinnedChats = chats.filter(c => c.pinned)
|
||||
const unpinnedChats = chats.filter(c => !c.pinned)
|
||||
@@ -36,6 +40,11 @@ export function ChatSidebar({
|
||||
onMobileClose?.()
|
||||
}
|
||||
|
||||
// When collapsed on desktop, render nothing — parent renders the top bar
|
||||
if (collapsed && !mobileOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile overlay */}
|
||||
@@ -52,14 +61,23 @@ export function ChatSidebar({
|
||||
style={{ background: 'var(--color-bg-sidebar)' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b shrink-0" style={{ borderColor: 'var(--color-border-default)' }}>
|
||||
<div className="px-3 py-3 border-b shrink-0 flex items-center gap-2" style={{ borderColor: 'var(--color-border-default)' }}>
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="w-full flex items-center justify-center gap-2 bg-primary text-white font-semibold text-sm rounded-lg px-4 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
className="flex-1 flex items-center justify-center gap-2 bg-primary text-white font-semibold text-sm rounded-lg px-4 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
<Plus size={16} />
|
||||
New Chat
|
||||
</button>
|
||||
{onToggleCollapse && (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="hidden sm:flex p-1.5 rounded-lg text-muted-foreground hover:text-heading hover:bg-elevated transition-colors"
|
||||
title="Collapse to top bar"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chat list */}
|
||||
@@ -108,6 +126,51 @@ export function ChatSidebar({
|
||||
)
|
||||
}
|
||||
|
||||
/** Collapsed top bar — rendered by the parent page above the chat area */
|
||||
export function ChatSidebarCollapsedBar({
|
||||
chats,
|
||||
activeChatId,
|
||||
onNewChat,
|
||||
onExpand,
|
||||
}: {
|
||||
chats: ChatListItem[]
|
||||
activeChatId: string | null
|
||||
onNewChat: () => void
|
||||
onExpand: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 border-b shrink-0"
|
||||
style={{ background: 'var(--color-bg-sidebar)', borderColor: 'var(--color-border-default)' }}
|
||||
>
|
||||
<button
|
||||
onClick={onNewChat}
|
||||
className="flex items-center gap-1.5 bg-primary text-white font-semibold text-xs rounded-md px-3 py-1.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
<Plus size={14} />
|
||||
New
|
||||
</button>
|
||||
<button
|
||||
onClick={onExpand}
|
||||
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:text-heading hover:bg-elevated transition-colors"
|
||||
title="Show chat history"
|
||||
>
|
||||
<History size={14} />
|
||||
<span>History</span>
|
||||
{chats.length > 0 && (
|
||||
<span className="text-[10px] bg-elevated rounded-full px-1.5 py-0.5 font-medium">{chats.length}</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
{activeChatId && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[300px]">
|
||||
{chats.find(c => c.id === activeChatId)?.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatItem({
|
||||
chat,
|
||||
isActive,
|
||||
|
||||
Reference in New Issue
Block a user