refactor: make sidebar a global component

This commit is contained in:
Haileyesus
2026-03-27 14:53:10 +03:00
parent 9a8178e9ca
commit 186dbcde63
6 changed files with 345 additions and 22 deletions

View File

@@ -1,37 +1,81 @@
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider, ProtectedRoute } from './components/auth';
import { TaskMasterProvider } from './contexts/TaskMasterContext';
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
import { WebSocketProvider } from './contexts/WebSocketContext';
import { PluginsProvider } from './contexts/PluginsContext';
import AppContent from './components/app/AppContent';
import i18n from './i18n/config.js';
import { RootLayout } from '@/components/refactored/shared/RootLayout';
// Mock page components
const Home = () => <div className="p-8"><h1>Home Page</h1><p>Select a session or create a new project.</p></div>;
const SessionContent = () => <div className="p-8"><h1>Session View</h1><p>Chat interface goes here.</p></div>;
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />, // The layout wraps all children
children: [
{
path: "/",
element: <Home />,
},
{
path: "/sessions/:sessionId",
element: <SessionContent />,
},
],
},
]);
export default function App() {
return (
<I18nextProvider i18n={i18n}>
<ThemeProvider>
<AuthProvider>
<WebSocketProvider>
<PluginsProvider>
<TasksSettingsProvider>
<TaskMasterProvider>
<ProtectedRoute>
<Router basename={window.__ROUTER_BASENAME__ || ''}>
<Routes>
<Route path="/" element={<AppContent />} />
<Route path="/session/:sessionId" element={<AppContent />} />
</Routes>
</Router>
</ProtectedRoute>
</TaskMasterProvider>
</TasksSettingsProvider>
</PluginsProvider>
</WebSocketProvider>
<ProtectedRoute>
<RouterProvider router={router} />
</ProtectedRoute>
</AuthProvider>
</ThemeProvider>
</I18nextProvider>
);
}
// import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
// import { I18nextProvider } from 'react-i18next';
// import { ThemeProvider } from './contexts/ThemeContext';
// import { AuthProvider, ProtectedRoute } from './components/auth';
// import { TaskMasterProvider } from './contexts/TaskMasterContext';
// import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
// import { WebSocketProvider } from './contexts/WebSocketContext';
// import { PluginsProvider } from './contexts/PluginsContext';
// import AppContent from './components/app/AppContent';
// import i18n from './i18n/config.js';
// export default function App() {
// return (
// <I18nextProvider i18n={i18n}>
// <ThemeProvider>
// <AuthProvider>
// <WebSocketProvider>
// 1<PluginsProvider>
// <TasksSettingsProvider>
// <TaskMasterProvider>
// <ProtectedRoute>
// <Router basename={window.__ROUTER_BASENAME__ || ''}>
// <Routes>
// <Route path="/" element={<AppContent />} />
// <Route path="/session/:sessionId" element={<AppContent />} />
// <Route path='/' element={<SampleElement />} />
// </Routes>
// </Router>
// </ProtectedRoute>
// </TaskMasterProvider>
// </TasksSettingsProvider>
// </PluginsProvider>
// </WebSocketProvider>
// </AuthProvider>
// </ThemeProvider>
// </I18nextProvider>
// );
// }

View File

@@ -0,0 +1,15 @@
import { Outlet } from 'react-router-dom';
import { Sidebar } from '@/components/refactored/sidebar/view/Sidebar';
export function RootLayout() {
return (
<div className="flex h-screen w-full overflow-hidden bg-background">
<Sidebar />
<main className="relative flex-1 overflow-hidden">
<Outlet />
</main>
</div>
);
}

View File

@@ -0,0 +1 @@
export type SearchMode = 'projects' | 'conversations';

View File

@@ -0,0 +1,60 @@
import { useState } from 'react';
import { PanelRightOpen } from 'lucide-react';
import { Button } from '@/shared/view/ui';
import { cn } from '@/lib/utils';
import SidebarHeader from '@/components/refactored/sidebar/view/SidebarHeader.js';
export function Sidebar() {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<>
{/* Mobile Backdrop Overlay - allows tapping outside to close */}
{!isCollapsed && (
<div
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm md:hidden"
onClick={() => setIsCollapsed(true)}
/>
)}
<aside
className={cn(
"flex flex-col bg-background/80 backdrop-blur-sm transition-all duration-300 border-r border-border h-full",
"fixed inset-y-0 left-0 z-50 md:relative md:z-0", // Make it fixed drawer on mobile, relative on desktop
isCollapsed
? "-translate-x-full md:translate-x-0 md:w-0 md:opacity-0 md:overflow-hidden md:border-none" // Hide fully on mobile if collapsed
: "translate-x-0 w-[85vw] sm:w-80 md:w-72 opacity-100"
)}
>
<SidebarHeader
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(true)}
/>
{/* Placeholder for the rest of the sidebar content */}
{!isCollapsed && (
<div className="flex-1 overflow-y-auto overscroll-contain">
{/* Future list component will go here */}
</div>
)}
</aside>
{/* Collapsed view handle - Only show on desktop since mobile hides it completely behind a toggle usually, but let's keep it consistent or standard.
Actually, on mobile, if it's completely hidden, we need a way to open it from the main content. For now we show the small bar if it's flex,
but since we made it fixed, let's keep the small bar fixed too. */}
{isCollapsed && (
<aside className="fixed inset-y-0 left-0 z-40 flex h-full flex-col items-center border-r border-border bg-background/80 px-2 py-4 md:relative">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground"
onClick={() => setIsCollapsed(false)}
title="Show Sidebar"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
</aside>
)}
</>
);
}

View File

@@ -0,0 +1,133 @@
import { useState } from 'react';
import { FolderPlus, Plus, RefreshCw, PanelLeftClose } from 'lucide-react';
import type { SearchMode } from '../types';
import { SidebarSearch } from './SidebarSearch';
import { Button } from '@/shared/view/ui';
import { cn } from '@/lib/utils';
import { IS_PLATFORM } from '@/constants/config';
type SidebarHeaderProps = {
isCollapsed: boolean;
onToggleCollapse: () => void;
};
export default function SidebarHeader({ isCollapsed, onToggleCollapse }: SidebarHeaderProps) {
// UI States declared here to avoid prop drilling as per instructions
const [searchMode, setSearchMode] = useState<SearchMode>('projects');
const [searchFilter, setSearchFilter] = useState('');
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefresh = () => {
setIsRefreshing(true);
setTimeout(() => setIsRefreshing(false), 1000);
};
const LogoBlock = () => (
<div className="flex min-w-0 items-center gap-2.5">
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg bg-primary/90 shadow-sm">
<svg className="h-3.5 w-3.5 text-primary-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</div>
<h1 className="truncate text-sm font-semibold tracking-tight text-foreground">Claude Code UI</h1>
</div>
);
const LogoWithLink = () => {
if (IS_PLATFORM) {
return (
<a
href="https://cloudcli.ai/dashboard"
className="flex min-w-0 items-center gap-2.5 transition-opacity hover:opacity-80 active:opacity-70"
title="View Environments Dashboard"
>
<LogoBlock />
</a>
);
}
return <LogoBlock />;
};
if (isCollapsed) return null;
return (
<div className="flex-shrink-0">
{/* Desktop header */}
<div className="hidden px-3 pb-2 pt-3 md:block">
<div className="flex items-center justify-between gap-2">
<LogoWithLink />
<div className="flex flex-shrink-0 items-center gap-0.5">
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground"
onClick={handleRefresh}
disabled={isRefreshing}
title="Refresh"
>
<RefreshCw className={cn("h-3.5 w-3.5", isRefreshing && "animate-spin")} />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground"
title="New Project"
>
<Plus className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground"
onClick={onToggleCollapse}
title="Hide Sidebar"
>
<PanelLeftClose className="h-3.5 w-3.5" />
</Button>
</div>
</div>
<SidebarSearch
searchMode={searchMode}
onSearchModeChange={setSearchMode}
searchFilter={searchFilter}
onSearchFilterChange={setSearchFilter}
/>
</div>
{/* Desktop divider */}
<div className="nav-divider hidden md:block" />
{/* Mobile header */}
<div className="p-3 pb-2 md:hidden">
<div className="flex items-center justify-between">
<LogoWithLink />
<div className="flex flex-shrink-0 gap-1.5">
<button
className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted/50 transition-all active:scale-95"
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw className={cn("h-4 w-4 text-muted-foreground", isRefreshing && "animate-spin")} />
</button>
<button
className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/90 text-primary-foreground transition-all active:scale-95"
>
<FolderPlus className="h-4 w-4" />
</button>
</div>
</div>
<SidebarSearch
searchMode={searchMode}
onSearchModeChange={setSearchMode}
searchFilter={searchFilter}
onSearchFilterChange={setSearchFilter}
/>
</div>
{/* Mobile divider */}
<div className="nav-divider md:hidden" />
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { Folder, MessageSquare, Search, X } from 'lucide-react';
import { Input } from '@/shared/view/ui';
import { cn } from '@/lib/utils';
import { SearchMode } from '@/components/refactored/sidebar/types/index.js';
type SidebarSearchProps = {
searchMode: SearchMode;
onSearchModeChange: (mode: SearchMode) => void;
searchFilter: string;
onSearchFilterChange: (filter: string) => void;
};
export function SidebarSearch({
searchMode,
onSearchModeChange,
searchFilter,
onSearchFilterChange
}: SidebarSearchProps) {
return (
<div className="mt-2.5 space-y-2">
<div className="flex rounded-lg bg-muted/50 p-0.5">
<button
onClick={() => onSearchModeChange('projects')}
className={cn(
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all",
searchMode === 'projects'
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
<Folder className="h-3 w-3" />
Projects
</button>
<button
onClick={() => onSearchModeChange('conversations')}
className={cn(
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all",
searchMode === 'conversations'
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
<MessageSquare className="h-3 w-3" />
Conversations
</button>
</div>
{/* Search bar */}
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/50 md:h-3.5 md:w-3.5" />
<Input
type="text"
placeholder={searchMode === 'conversations' ? "Search conversations..." : "Search projects..."}
value={searchFilter}
onChange={(event) => onSearchFilterChange(event.target.value)}
className="nav-search-input h-10 rounded-xl border-0 pl-10 pr-9 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 md:h-9 md:pl-9 md:pr-8"
/>
{searchFilter && (
<button
onClick={() => onSearchFilterChange('')}
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded-md p-1 hover:bg-accent md:p-0.5"
>
<X className="h-3.5 w-3.5 text-muted-foreground md:h-3 md:w-3" />
</button>
)}
</div>
</div>
);
}