Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions apps/agent/components/sidebar/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
import type { FC } from 'react'
import { useCallback } from 'react'
import { cn } from '@/lib/utils'
import { SidebarBranding } from './SidebarBranding'
import { SidebarNavigation } from './SidebarNavigation'
import { SidebarUserFooter } from './SidebarUserFooter'

interface AppSidebarProps {
expanded?: boolean
onToggle?: () => void
onOpenShortcuts?: () => void
}

export const AppSidebar: FC<AppSidebarProps> = ({
expanded = false,
onToggle,
onOpenShortcuts,
}) => {
const handleSidebarClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement
if (target.closest('[data-sidebar-interactive]')) {
return
}
onToggle?.()
},
[onToggle],
)

return (
<div
className={cn(
'flex h-full flex-col border-r bg-sidebar text-sidebar-foreground transition-all duration-200 ease-in-out',
expanded ? 'w-64' : 'w-14',
)}
>
<SidebarBranding expanded={expanded} />
<SidebarNavigation expanded={expanded} />
<SidebarUserFooter
expanded={expanded}
onOpenShortcuts={onOpenShortcuts}
/>
</div>
<>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: sidebar toggles via empty-space click */}
<aside
onClick={handleSidebarClick}
className={cn(
'relative z-20 flex min-h-screen shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground transition-all duration-200 ease-in-out',
expanded ? 'w-64' : 'w-14',
)}
>
<SidebarBranding expanded={expanded} onToggle={onToggle} />
<SidebarNavigation expanded={expanded} />
<SidebarUserFooter
expanded={expanded}
onOpenShortcuts={onOpenShortcuts}
/>
</aside>
</>
)
}
21 changes: 17 additions & 4 deletions apps/agent/components/sidebar/SidebarBranding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ import { useWorkspace } from '@/lib/workspace/use-workspace'

interface SidebarBrandingProps {
expanded?: boolean
onToggle?: () => void
}

export const SidebarBranding: FC<SidebarBrandingProps> = ({
expanded = true,
onToggle,
}) => {
const { selectedFolder } = useWorkspace()

return (
<div className="flex h-14 items-center border-b">
<div className="flex w-14 shrink-0 items-center justify-center">
<img src={ProductLogo} alt="BrowserOS" className="size-8" />
</div>
<button
type="button"
onClick={(event) => {
event.stopPropagation()
onToggle?.()
}}
className="flex w-14 shrink-0 items-center justify-center transition-opacity hover:opacity-70 active:opacity-50"
aria-label={expanded ? 'Collapse sidebar' : 'Expand sidebar'}
data-sidebar-toggle
>
<img src={ProductLogo} alt="BrowserOS" className="size-9" />
</button>
<div
className={cn(
'flex min-w-0 flex-1 items-center justify-between pr-3 transition-opacity duration-200',
Expand All @@ -30,7 +41,9 @@ export const SidebarBranding: FC<SidebarBrandingProps> = ({
</span>
<span className="text-muted-foreground text-xs">Personal</span>
</div>
<ThemeToggle className="h-8 w-8 shrink-0" iconClassName="h-4 w-4" />
<div data-sidebar-interactive>
<ThemeToggle className="h-8 w-8 shrink-0" iconClassName="h-4 w-4" />
</div>
</div>
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion apps/agent/components/sidebar/SidebarNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ export const SidebarNavigation: FC<SidebarNavigationProps> = ({
const navItem = (
<NavLink
to={item.to}
data-sidebar-interactive
className={cn(
'flex h-9 items-center gap-2 overflow-hidden whitespace-nowrap rounded-md px-3 font-medium text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
isActive &&
'bg-sidebar-accent text-sidebar-accent-foreground',
)}
>
<Icon className="size-4 shrink-0" />
<Icon className="size-5 shrink-0" />
<span
className={cn(
'truncate transition-opacity duration-200',
Expand Down
6 changes: 4 additions & 2 deletions apps/agent/components/sidebar/SidebarUserFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ export const SidebarUserFooter: FC<SidebarUserFooterProps> = ({
href="https://docs.browseros.com/"
target="_blank"
rel="noopener noreferrer"
data-sidebar-interactive
className="flex h-9 items-center gap-2 overflow-hidden whitespace-nowrap rounded-md px-3 font-medium text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<Info className="size-4 shrink-0" />
<Info className="size-5 shrink-0" />
<span
className={cn(
'truncate transition-opacity duration-200',
Expand All @@ -59,9 +60,10 @@ export const SidebarUserFooter: FC<SidebarUserFooterProps> = ({
<Button
variant="ghost"
onClick={onOpenShortcuts}
data-sidebar-interactive
className="flex h-9 w-full items-center justify-start gap-2 overflow-hidden whitespace-nowrap rounded-md px-3 font-medium text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<Keyboard className="size-4 shrink-0" />
<Keyboard className="size-5 shrink-0" />
<span
className={cn(
'truncate transition-opacity duration-200',
Expand Down
62 changes: 24 additions & 38 deletions apps/agent/entrypoints/app/layout/SidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Menu } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { Outlet, useLocation } from 'react-router'
import { AppSidebar } from '@/components/sidebar/AppSidebar'
import { Button } from '@/components/ui/button'
Expand All @@ -11,15 +11,26 @@ import { SETTINGS_PAGE_VIEWED_EVENT } from '@/lib/constants/analyticsEvents'
import { track } from '@/lib/metrics/track'
import { RpcClientProvider } from '@/lib/rpc/RpcClientProvider'

const COLLAPSE_DELAY = 150
const SIDEBAR_STORAGE_KEY = 'browseros-sidebar-expanded'

export const SidebarLayout: FC = () => {
const location = useLocation()
const isMobile = useIsMobile()
const [sidebarOpen, setSidebarOpen] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(() => {
// Default to collapsed (false) on first load
const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY)
return stored === 'true'
})
const [mobileOpen, setMobileOpen] = useState(false)
const [shortcutsDialogOpen, setShortcutsDialogOpen] = useState(false)
const collapseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const toggleSidebar = useCallback(() => {
setSidebarOpen((prev) => {
const newValue = !prev
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(newValue))
return newValue
})
}, [])

const openShortcuts = useCallback(() => {
setShortcutsDialogOpen(true)
Expand All @@ -33,28 +44,6 @@ export const SidebarLayout: FC = () => {
setMobileOpen(false)
}, [])

useEffect(() => {
return () => {
if (collapseTimeoutRef.current) {
clearTimeout(collapseTimeoutRef.current)
}
}
}, [])

const handleMouseEnter = useCallback(() => {
if (collapseTimeoutRef.current) {
clearTimeout(collapseTimeoutRef.current)
collapseTimeoutRef.current = null
}
setSidebarOpen(true)
}, [])

const handleMouseLeave = useCallback(() => {
collapseTimeoutRef.current = setTimeout(() => {
setSidebarOpen(false)
}, COLLAPSE_DELAY)
}, [])

if (isMobile) {
return (
<RpcClientProvider>
Expand Down Expand Up @@ -91,19 +80,16 @@ export const SidebarLayout: FC = () => {

return (
<RpcClientProvider>
<div className="relative min-h-screen bg-background">
{/* Sidebar - fixed overlay */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: hover interactions needed */}
<div
className="fixed inset-y-0 left-0 z-40"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<AppSidebar expanded={sidebarOpen} onOpenShortcuts={openShortcuts} />
</div>
<div className="flex min-h-screen bg-background">
{/* Sidebar - push mode */}
<AppSidebar
expanded={sidebarOpen}
onToggle={toggleSidebar}
onOpenShortcuts={openShortcuts}
/>

{/* Main content - full width, centered */}
<main className="min-h-screen overflow-y-auto">
{/* Main content - adjusts based on sidebar width */}
<main className="relative min-h-screen flex-1 overflow-y-auto">
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
<Outlet />
</div>
Expand Down