Files
claudecodeui/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
2026-06-17 22:15:36 +00:00

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>
);
}