refactor(desktop): redesign layout with sidebar navigation
- Replace tab navigation with collapsible sidebar - Fix header scroll issue with proper flex container structure - Add TooltipProvider at app root - Simplify onboarding store by removing forceOnboarding flag Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bbf2a16f39
commit
bbf91a98ca
3 changed files with 90 additions and 75 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect } from 'react'
|
||||
import { createHashRouter, Navigate, RouterProvider } from 'react-router-dom'
|
||||
import { ThemeProvider } from './components/theme-provider'
|
||||
import { TooltipProvider } from '@multica/ui/components/ui/tooltip'
|
||||
import Layout from './pages/layout'
|
||||
import HomePage from './pages/home'
|
||||
import ChatPage from './pages/chat'
|
||||
|
|
@ -15,8 +16,7 @@ import { useChannelsStore } from './stores/channels'
|
|||
|
||||
function OnboardingGuard({ children }: { children: React.ReactNode }) {
|
||||
const completed = useOnboardingStore((s) => s.completed)
|
||||
const forceOnboarding = useOnboardingStore((s) => s.forceOnboarding)
|
||||
if (!completed || forceOnboarding) return <Navigate to="/onboarding" replace />
|
||||
if (!completed) return <Navigate to="/onboarding" replace />
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +48,6 @@ const router = createHashRouter([
|
|||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
useOnboardingStore.getState().initForceFlag()
|
||||
// Prefetch global data at app startup
|
||||
useProviderStore.getState().fetch()
|
||||
useChannelsStore.getState().fetch()
|
||||
|
|
@ -56,7 +55,9 @@ export default function App() {
|
|||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="system" storageKey="multica-theme">
|
||||
<RouterProvider router={router} />
|
||||
<TooltipProvider>
|
||||
<RouterProvider router={router} />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +1,114 @@
|
|||
import { Outlet, NavLink, useLocation } from 'react-router-dom'
|
||||
import { Toaster } from '@multica/ui/components/ui/sonner'
|
||||
import { Button } from '@multica/ui/components/ui/button'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import {
|
||||
Settings02Icon,
|
||||
Home01Icon,
|
||||
Comment01Icon,
|
||||
CodeIcon,
|
||||
PlugIcon,
|
||||
Comment01Icon,
|
||||
Share08Icon,
|
||||
Time04Icon,
|
||||
} from '@hugeicons/core-free-icons'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from '@multica/ui/components/ui/sidebar'
|
||||
import { cn } from '@multica/ui/lib/utils'
|
||||
import { ModeToggle } from '../components/mode-toggle'
|
||||
import { DeviceConfirmDialog } from '../components/device-confirm-dialog'
|
||||
import ChatPage from './chat'
|
||||
|
||||
const tabs = [
|
||||
{ path: '/', label: 'Home', icon: Home01Icon, exact: true },
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Home', icon: Home01Icon },
|
||||
{ path: '/chat', label: 'Chat', icon: Comment01Icon },
|
||||
{ path: '/tools', label: 'Tools', icon: CodeIcon },
|
||||
{ path: '/skills', label: 'Skills', icon: PlugIcon },
|
||||
{ path: '/channels', label: 'Channels', icon: Share08Icon },
|
||||
{ path: '/crons', label: 'Cron', icon: Time04Icon },
|
||||
{ path: '/crons', label: 'Crons', icon: Time04Icon },
|
||||
]
|
||||
|
||||
function MainHeader() {
|
||||
const { state, isMobile } = useSidebar()
|
||||
const needsTrafficLightSpace = state === 'collapsed' || isMobile
|
||||
|
||||
return (
|
||||
<header className="h-12 shrink-0 flex items-center px-4">
|
||||
{/* Drag placeholder for traffic lights when sidebar is collapsed */}
|
||||
<div
|
||||
className={cn(
|
||||
'h-full shrink-0 transition-[width] duration-200 ease-linear',
|
||||
needsTrafficLightSpace ? 'w-16' : 'w-0'
|
||||
)}
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
/>
|
||||
|
||||
<SidebarTrigger />
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right: Theme toggle */}
|
||||
<ModeToggle />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div className="h-dvh flex flex-col bg-background">
|
||||
{/* Header with drag region for macOS */}
|
||||
<header
|
||||
className="flex items-center justify-between px-4 py-3 border-b pl-20"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold">Multica</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
|
||||
>
|
||||
<Button variant="ghost" size="icon">
|
||||
<HugeiconsIcon icon={Settings02Icon} className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex h-screen flex-col bg-background text-foreground">
|
||||
<SidebarProvider className="flex-1 overflow-hidden">
|
||||
<Sidebar>
|
||||
{/* Traffic light area */}
|
||||
<SidebarHeader
|
||||
className="h-12 shrink-0"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<nav className="flex gap-1 px-4 py-2 border-b">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.exact
|
||||
? location.pathname === tab.path
|
||||
: location.pathname.startsWith(tab.path)
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
{navItems.map((item) => {
|
||||
const isActive = item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
return (
|
||||
<SidebarMenuItem key={item.path}>
|
||||
<NavLink to={item.path}>
|
||||
<SidebarMenuButton isActive={isActive}>
|
||||
<HugeiconsIcon icon={item.icon} className="size-4" />
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
|
||||
return (
|
||||
<NavLink key={tab.path} to={tab.path}>
|
||||
<Button
|
||||
variant={isActive ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
className={cn('gap-2', isActive && 'bg-secondary')}
|
||||
>
|
||||
<HugeiconsIcon icon={tab.icon} className="size-4" />
|
||||
{tab.label}
|
||||
</Button>
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<SidebarInset className="overflow-hidden">
|
||||
<MainHeader />
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 overflow-auto relative">
|
||||
{/* ChatPage is always mounted (cached), hidden via CSS */}
|
||||
<div className={cn('absolute inset-0', location.pathname === '/chat' ? '' : 'hidden')}>
|
||||
<ChatPage />
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-hidden min-h-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
</SidebarInset>
|
||||
|
||||
{/* Other routes render normally via Outlet */}
|
||||
{location.pathname !== '/chat' && (
|
||||
<div className="p-4 h-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
<Toaster />
|
||||
<DeviceConfirmDialog />
|
||||
<Toaster />
|
||||
<DeviceConfirmDialog />
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ interface AcknowledgementsState {
|
|||
|
||||
interface OnboardingStore {
|
||||
completed: boolean
|
||||
forceOnboarding: boolean
|
||||
currentStep: number
|
||||
acknowledgements: AcknowledgementsState
|
||||
allAcknowledged: boolean
|
||||
|
|
@ -23,14 +22,12 @@ interface OnboardingStore {
|
|||
setProviderConfigured: (configured: boolean) => void
|
||||
setClientConnected: (connected: boolean) => void
|
||||
completeOnboarding: () => void
|
||||
initForceFlag: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useOnboardingStore = create<OnboardingStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
completed: false,
|
||||
forceOnboarding: false,
|
||||
currentStep: 0,
|
||||
|
||||
acknowledgements: {
|
||||
|
|
@ -57,14 +54,7 @@ export const useOnboardingStore = create<OnboardingStore>()(
|
|||
|
||||
setClientConnected: (connected) => set({ clientConnected: connected }),
|
||||
|
||||
completeOnboarding: () => set({ completed: true, forceOnboarding: false, currentStep: 0 }),
|
||||
|
||||
initForceFlag: async () => {
|
||||
const flags = await window.electronAPI.app.getFlags()
|
||||
if (flags.forceOnboarding) {
|
||||
set({ forceOnboarding: true })
|
||||
}
|
||||
},
|
||||
completeOnboarding: () => set({ completed: true, currentStep: 0 }),
|
||||
}),
|
||||
{
|
||||
name: 'multica-onboarding',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue