feat(layout): add three-column layout with collapsible right panel

- Add unified uiStore for sidebar and right panel state management
- Create RightPanel component with placeholder content
- Add SidebarTrigger and RightPanelTrigger with secondary variant when open
- Right panel only shows on lg screens (1024px+) to avoid clutter
- State persisted via localStorage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-01-14 20:55:00 +08:00
parent be42a96845
commit a1ee14da31
5 changed files with 206 additions and 4 deletions

View File

@@ -9,6 +9,12 @@ import { Modals } from './components/Modals'
import { ThemeProvider } from './contexts/ThemeContext'
import { SidebarProvider } from '@/components/ui/sidebar'
import { useModalStore } from './stores/modalStore'
import { useUIStore } from './stores/uiStore'
import {
RightPanel,
RightPanelHeader,
RightPanelContent,
} from './components/layout'
function AppContent(): React.JSX.Element {
const {
@@ -31,6 +37,10 @@ function AppContent(): React.JSX.Element {
const openModal = useModalStore((s) => s.openModal)
// UI state
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
// Default agent for new sessions
const [defaultAgentId, setDefaultAgentId] = useState('opencode')
@@ -73,7 +83,11 @@ function AppContent(): React.JSX.Element {
)}
{/* Main content */}
<SidebarProvider className="flex-1 overflow-hidden">
<SidebarProvider
open={sidebarOpen}
onOpenChange={setSidebarOpen}
className="flex-1 overflow-hidden"
>
{/* Sidebar */}
<AppSidebar
sessions={sessions}
@@ -83,7 +97,7 @@ function AppContent(): React.JSX.Element {
/>
{/* Main area */}
<main className="flex flex-1 flex-col">
<main className="flex min-w-0 flex-1 flex-col">
{/* Status bar */}
<StatusBar
runningSessionsCount={runningSessionsStatus.runningSessions}
@@ -107,6 +121,18 @@ function AppContent(): React.JSX.Element {
disabled={!currentSession}
/>
</main>
{/* Right panel - placeholder for future content */}
<RightPanel>
<RightPanelHeader>
<span className="text-sm font-medium">Details</span>
</RightPanelHeader>
<RightPanelContent>
<div className="flex h-full items-center justify-center text-muted-foreground">
<p className="text-sm">Right panel content</p>
</div>
</RightPanelContent>
</RightPanel>
</SidebarProvider>
{/* Global modals */}

View File

@@ -2,7 +2,8 @@
* Status bar component - shows session info and running status
*/
import type { MulticaSession } from '../../../shared/types'
import { SidebarTrigger, useSidebar } from '@/components/ui/sidebar'
import { useSidebar } from '@/components/ui/sidebar'
import { SidebarTrigger, RightPanelTrigger } from './layout'
import { cn } from '@/lib/utils'
interface StatusBarProps {
@@ -43,12 +44,13 @@ export function StatusBar({
)}
</div>
{/* Right: Status */}
{/* Right: Status + Right panel trigger */}
<div className="titlebar-no-drag flex items-center gap-3">
<SessionStatusBadge
isRunning={isCurrentSessionRunning}
runningCount={runningSessionsCount}
/>
<RightPanelTrigger />
</div>
</div>
)

View File

@@ -0,0 +1,129 @@
/**
* Layout components - panels and triggers
* Desktop only - hidden on mobile
*/
import * as React from 'react'
import { PanelLeftIcon, PanelRightIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { useUIStore } from '@/stores/uiStore'
import { useSidebar } from '@/components/ui/sidebar'
const RIGHT_PANEL_WIDTH = '20rem' // 320px
interface RightPanelProps {
children: React.ReactNode
className?: string
}
export function RightPanel({ children, className }: RightPanelProps) {
const isOpen = useUIStore((s) => s.rightPanelOpen)
return (
<div
className={cn(
'hidden lg:block',
'transition-[width] duration-200 ease-linear',
isOpen ? 'w-[var(--right-panel-width)]' : 'w-0'
)}
style={{ '--right-panel-width': RIGHT_PANEL_WIDTH } as React.CSSProperties}
>
<div
className={cn(
'fixed inset-y-0 right-0 z-10 h-svh border-l bg-background',
'transition-[transform,opacity] duration-200 ease-linear',
isOpen ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0',
className
)}
style={{ width: RIGHT_PANEL_WIDTH }}
>
{children}
</div>
</div>
)
}
// Trigger button - desktop only, secondary variant when panel is open
export function RightPanelTrigger({
className,
...props
}: React.ComponentProps<typeof Button>) {
const isOpen = useUIStore((s) => s.rightPanelOpen)
const toggle = useUIStore((s) => s.toggleRightPanel)
return (
<Button
variant={isOpen ? 'secondary' : 'ghost'}
size="icon"
className={cn('hidden lg:inline-flex size-7', className)}
onClick={toggle}
{...props}
>
<PanelRightIcon className="h-4 w-4" />
<span className="sr-only">Toggle Right Panel</span>
</Button>
)
}
// Sidebar trigger - uses uiStore, secondary variant when open
export function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
const isOpen = useUIStore((s) => s.sidebarOpen)
return (
<Button
variant={isOpen ? 'secondary' : 'ghost'}
size="icon"
className={cn('size-7', className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon className="h-4 w-4" />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
// Sub-components for consistent structure
export function RightPanelHeader({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
className={cn('flex h-11 items-center border-b px-4', className)}
{...props}
/>
)
}
export function RightPanelContent({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
className={cn('flex-1 overflow-auto p-4', className)}
{...props}
/>
)
}
export function RightPanelFooter({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
className={cn('border-t p-4', className)}
{...props}
/>
)
}

View File

@@ -0,0 +1,8 @@
export {
RightPanel,
RightPanelTrigger,
RightPanelHeader,
RightPanelContent,
RightPanelFooter,
SidebarTrigger,
} from './RightPanel'

View File

@@ -0,0 +1,37 @@
/**
* Global UI state management using Zustand
* Stores all UI-related state like panel visibility, etc.
*/
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface UIStore {
// Sidebar state
sidebarOpen: boolean
toggleSidebar: () => void
setSidebarOpen: (open: boolean) => void
// Right panel state
rightPanelOpen: boolean
toggleRightPanel: () => void
setRightPanelOpen: (open: boolean) => void
}
export const useUIStore = create<UIStore>()(
persist(
(set) => ({
// Sidebar - default open
sidebarOpen: true,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
// Right panel - default open
rightPanelOpen: true,
toggleRightPanel: () => set((state) => ({ rightPanelOpen: !state.rightPanelOpen })),
setRightPanelOpen: (open) => set({ rightPanelOpen: open }),
}),
{
name: 'ui-state', // localStorage key
}
)
)