mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 17:16:19 +00:00
refactor: make sidebar a global component
This commit is contained in:
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