mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 02:15:34 +08:00
refactor: make sidebar a global component
This commit is contained in:
88
src/App.tsx
88
src/App.tsx
@@ -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 { I18nextProvider } from 'react-i18next';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { AuthProvider, ProtectedRoute } from './components/auth';
|
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 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() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<WebSocketProvider>
|
<ProtectedRoute>
|
||||||
<PluginsProvider>
|
<RouterProvider router={router} />
|
||||||
<TasksSettingsProvider>
|
</ProtectedRoute>
|
||||||
<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>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</I18nextProvider>
|
</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>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|||||||
15
src/components/refactored/shared/RootLayout.tsx
Normal file
15
src/components/refactored/shared/RootLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/refactored/sidebar/types/index.ts
Normal file
1
src/components/refactored/sidebar/types/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type SearchMode = 'projects' | 'conversations';
|
||||||
60
src/components/refactored/sidebar/view/Sidebar.tsx
Normal file
60
src/components/refactored/sidebar/view/Sidebar.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
src/components/refactored/sidebar/view/SidebarHeader.tsx
Normal file
133
src/components/refactored/sidebar/view/SidebarHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/refactored/sidebar/view/SidebarSearch.tsx
Normal file
70
src/components/refactored/sidebar/view/SidebarSearch.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user