mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
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:
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
129
src/renderer/src/components/layout/RightPanel.tsx
Normal file
129
src/renderer/src/components/layout/RightPanel.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
8
src/renderer/src/components/layout/index.ts
Normal file
8
src/renderer/src/components/layout/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
RightPanel,
|
||||
RightPanelTrigger,
|
||||
RightPanelHeader,
|
||||
RightPanelContent,
|
||||
RightPanelFooter,
|
||||
SidebarTrigger,
|
||||
} from './RightPanel'
|
||||
37
src/renderer/src/stores/uiStore.ts
Normal file
37
src/renderer/src/stores/uiStore.ts
Normal 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
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user