mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-20 16:02:05 +08:00
372 lines
16 KiB
TypeScript
372 lines
16 KiB
TypeScript
import { Activity, Archive, Folder, FolderPlus, MessageSquare, Plus, RefreshCw, Search, X, PanelLeftClose } from 'lucide-react';
|
|
import type { TFunction } from 'i18next';
|
|
|
|
import { Button, Input, Tooltip } from '../../../../shared/view/ui';
|
|
import { IS_PLATFORM } from '../../../../constants/config';
|
|
import { cn } from '../../../../lib/utils';
|
|
import type { SidebarSearchMode } from '../../types/types';
|
|
|
|
import GitHubStarBadge from './GitHubStarBadge';
|
|
|
|
const MOD_KEY =
|
|
typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘' : 'Ctrl';
|
|
|
|
type SidebarHeaderProps = {
|
|
isPWA: boolean;
|
|
isMobile: boolean;
|
|
isLoading: boolean;
|
|
projectsCount: number;
|
|
runningSessionsCount: number;
|
|
archivedSessionsCount: number;
|
|
isArchivedSessionsLoading: boolean;
|
|
searchFilter: string;
|
|
onSearchFilterChange: (value: string) => void;
|
|
onClearSearchFilter: () => void;
|
|
searchMode: SidebarSearchMode;
|
|
onSearchModeChange: (mode: SidebarSearchMode) => void;
|
|
onRefresh: () => void;
|
|
isRefreshing: boolean;
|
|
onCreateProject: () => void;
|
|
onCollapseSidebar: () => void;
|
|
t: TFunction;
|
|
};
|
|
|
|
export default function SidebarHeader({
|
|
isPWA,
|
|
isMobile,
|
|
isLoading,
|
|
projectsCount,
|
|
runningSessionsCount,
|
|
archivedSessionsCount,
|
|
isArchivedSessionsLoading,
|
|
searchFilter,
|
|
onSearchFilterChange,
|
|
onClearSearchFilter,
|
|
searchMode,
|
|
onSearchModeChange,
|
|
onRefresh,
|
|
isRefreshing,
|
|
onCreateProject,
|
|
onCollapseSidebar,
|
|
t,
|
|
}: SidebarHeaderProps) {
|
|
const showSearchTools = (projectsCount > 0 || runningSessionsCount > 0 || archivedSessionsCount > 0 || isArchivedSessionsLoading) && !isLoading;
|
|
const searchPlaceholder = searchMode === 'conversations'
|
|
? t('search.conversationsPlaceholder')
|
|
: searchMode === 'archived'
|
|
? t('search.archivedPlaceholder', 'Search archived sessions...')
|
|
: searchMode === 'running'
|
|
? t('search.runningPlaceholder', 'Search running sessions...')
|
|
: t('projects.searchPlaceholder');
|
|
const runningBadgeText = runningSessionsCount > 99 ? '99+' : String(runningSessionsCount);
|
|
|
|
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-normal tracking-tight text-foreground">{t('app.title')}</h1>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="flex-shrink-0">
|
|
{/* Desktop header */}
|
|
<div
|
|
className="hidden px-3 pb-2 pt-3 md:block"
|
|
style={{}}
|
|
>
|
|
<div className="flex items-center justify-between gap-2">
|
|
{IS_PLATFORM ? (
|
|
<a
|
|
href="https://cloudcli.ai/dashboard"
|
|
className="flex min-w-0 items-center gap-2.5 transition-opacity hover:opacity-80"
|
|
title={t('tooltips.viewEnvironments')}
|
|
>
|
|
<LogoBlock />
|
|
</a>
|
|
) : (
|
|
<LogoBlock />
|
|
)}
|
|
|
|
<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={onRefresh}
|
|
disabled={isRefreshing}
|
|
title={t('tooltips.refresh')}
|
|
>
|
|
<RefreshCw
|
|
className={`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"
|
|
onClick={onCreateProject}
|
|
title={t('tooltips.createProject')}
|
|
>
|
|
<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={onCollapseSidebar}
|
|
title={t('tooltips.hideSidebar')}
|
|
>
|
|
<PanelLeftClose className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<GitHubStarBadge />
|
|
|
|
{/* Search bar */}
|
|
{showSearchTools && (
|
|
<div className="mt-2.5 space-y-2">
|
|
{/* Search mode toggle */}
|
|
<div className="flex rounded-lg bg-muted/50 p-0.5">
|
|
<button
|
|
onClick={() => onSearchModeChange('projects')}
|
|
aria-pressed={searchMode === 'projects'}
|
|
className={cn(
|
|
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'projects'
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<Folder className="h-3 w-3" />
|
|
{t('search.modeProjects')}
|
|
</button>
|
|
<button
|
|
onClick={() => onSearchModeChange('conversations')}
|
|
aria-pressed={searchMode === 'conversations'}
|
|
className={cn(
|
|
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'conversations'
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<MessageSquare className="h-3 w-3" />
|
|
{t('search.modeConversations')}
|
|
</button>
|
|
<Tooltip content={t('search.runningTooltip', 'Running sessions')} position="top">
|
|
<button
|
|
onClick={() => onSearchModeChange('running')}
|
|
aria-pressed={searchMode === 'running'}
|
|
aria-label={t('search.runningTooltip', 'Running sessions')}
|
|
title={t('search.runningTooltip', 'Running sessions')}
|
|
className={cn(
|
|
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'running'
|
|
? "bg-background shadow-sm text-foreground ring-1 ring-emerald-500/15"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<span className="relative flex h-3 w-3 items-center justify-center">
|
|
<Activity className={cn("h-3 w-3", runningSessionsCount > 0 && "text-emerald-500")} />
|
|
{runningSessionsCount > 0 && (
|
|
<span className="absolute -right-2.5 -top-2 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-emerald-500 px-0.5 text-[8px] font-semibold leading-none text-white shadow-sm ring-1 ring-background">
|
|
{runningBadgeText}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</button>
|
|
</Tooltip>
|
|
<Tooltip content={t('search.archiveOnlyTooltip', 'Archive only')} position="top">
|
|
<button
|
|
onClick={() => onSearchModeChange('archived')}
|
|
aria-pressed={searchMode === 'archived'}
|
|
aria-label={t('search.archiveOnlyTooltip', 'Archive only')}
|
|
title={t('search.archiveOnlyTooltip', 'Archive only')}
|
|
className={cn(
|
|
"flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'archived'
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<Archive className="h-3 w-3" />
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
<div className="relative">
|
|
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/50" />
|
|
<Input
|
|
type="text"
|
|
placeholder={searchPlaceholder}
|
|
value={searchFilter}
|
|
onChange={(event) => onSearchFilterChange(event.target.value)}
|
|
className="nav-search-input h-9 rounded-xl border-0 pl-9 pr-14 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
/>
|
|
{searchFilter ? (
|
|
<button
|
|
onClick={onClearSearchFilter}
|
|
aria-label={t('tooltips.clearSearch')}
|
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded-md p-0.5 hover:bg-accent"
|
|
>
|
|
<X className="h-3 w-3 text-muted-foreground" />
|
|
</button>
|
|
) : (
|
|
<kbd
|
|
aria-hidden
|
|
title={t('tooltips.openCommandPalette')}
|
|
className="pointer-events-none absolute right-2.5 top-1/2 hidden -translate-y-1/2 items-center gap-0.5 rounded border border-border/60 bg-muted/40 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground md:inline-flex"
|
|
>
|
|
{MOD_KEY}
|
|
<span>K</span>
|
|
</kbd>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Desktop divider */}
|
|
<div className="nav-divider hidden md:block" />
|
|
|
|
{/* Mobile header */}
|
|
<div
|
|
className="p-3 pb-2 md:hidden"
|
|
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
{IS_PLATFORM ? (
|
|
<a
|
|
href="https://cloudcli.ai/dashboard"
|
|
className="flex min-w-0 items-center gap-2.5 transition-opacity active:opacity-70"
|
|
title={t('tooltips.viewEnvironments')}
|
|
>
|
|
<LogoBlock />
|
|
</a>
|
|
) : (
|
|
<LogoBlock />
|
|
)}
|
|
|
|
<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={onRefresh}
|
|
disabled={isRefreshing}
|
|
>
|
|
<RefreshCw className={`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"
|
|
onClick={onCreateProject}
|
|
>
|
|
<FolderPlus className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile search */}
|
|
{showSearchTools && (
|
|
<div className="mt-2.5 space-y-2">
|
|
<div className="flex rounded-lg bg-muted/50 p-0.5">
|
|
<button
|
|
onClick={() => onSearchModeChange('projects')}
|
|
aria-pressed={searchMode === 'projects'}
|
|
className={cn(
|
|
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'projects'
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<Folder className="h-3 w-3" />
|
|
{t('search.modeProjects')}
|
|
</button>
|
|
<button
|
|
onClick={() => onSearchModeChange('conversations')}
|
|
aria-pressed={searchMode === 'conversations'}
|
|
className={cn(
|
|
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'conversations'
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<MessageSquare className="h-3 w-3" />
|
|
{t('search.modeConversations')}
|
|
</button>
|
|
<Tooltip content={t('search.runningTooltip', 'Running sessions')} position="top">
|
|
<button
|
|
onClick={() => onSearchModeChange('running')}
|
|
aria-pressed={searchMode === 'running'}
|
|
aria-label={t('search.runningTooltip', 'Running sessions')}
|
|
title={t('search.runningTooltip', 'Running sessions')}
|
|
className={cn(
|
|
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'running'
|
|
? "bg-background shadow-sm text-foreground ring-1 ring-emerald-500/15"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<span className="relative flex h-3 w-3 items-center justify-center">
|
|
<Activity className={cn("h-3 w-3", runningSessionsCount > 0 && "text-emerald-500")} />
|
|
{runningSessionsCount > 0 && (
|
|
<span className="absolute -right-2.5 -top-2 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-emerald-500 px-0.5 text-[8px] font-semibold leading-none text-white shadow-sm ring-1 ring-background">
|
|
{runningBadgeText}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span className="sr-only">{t('search.modeRunning', 'Running')}</span>
|
|
</button>
|
|
</Tooltip>
|
|
<Tooltip content={t('search.archiveOnlyTooltip', 'Archive only')} position="top">
|
|
<button
|
|
onClick={() => onSearchModeChange('archived')}
|
|
aria-pressed={searchMode === 'archived'}
|
|
aria-label={t('search.archiveOnlyTooltip', 'Archive only')}
|
|
title={t('search.archiveOnlyTooltip', 'Archive only')}
|
|
className={cn(
|
|
"flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-normal transition-all",
|
|
searchMode === 'archived'
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<Archive className="h-3 w-3" />
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
<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" />
|
|
<Input
|
|
type="text"
|
|
placeholder={searchPlaceholder}
|
|
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"
|
|
/>
|
|
{searchFilter && (
|
|
<button
|
|
onClick={onClearSearchFilter}
|
|
aria-label={t('tooltips.clearSearch')}
|
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded-md p-1 hover:bg-accent"
|
|
>
|
|
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mobile divider */}
|
|
<div className="nav-divider md:hidden" />
|
|
</div>
|
|
);
|
|
}
|