feat: redesigned Git tab layout with improved organization
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RiArrowUpLine,
|
||||
RiArrowDownLine,
|
||||
RiArrowDownSLine,
|
||||
RiLoader4Line,
|
||||
RiGitBranchLine,
|
||||
RiBriefcaseLine,
|
||||
RiHomeLine,
|
||||
RiGraduationCapLine,
|
||||
RiCodeLine,
|
||||
RiHeartLine,
|
||||
RiUser3Line,
|
||||
} from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { BranchSelector } from './BranchSelector';
|
||||
import { SyncActions } from './SyncActions';
|
||||
import type { GitStatus, GitIdentityProfile } from '@/lib/api/types';
|
||||
|
||||
type SyncAction = 'fetch' | 'pull' | 'push' | null;
|
||||
|
||||
interface GitHeaderProps {
|
||||
status: GitStatus | null;
|
||||
localBranches: string[];
|
||||
remoteBranches: string[];
|
||||
branchInfo: Record<string, { ahead?: number; behind?: number }> | undefined;
|
||||
syncAction: SyncAction;
|
||||
onFetch: () => void;
|
||||
onPull: () => void;
|
||||
onPush: () => void;
|
||||
onCheckoutBranch: (branch: string) => void;
|
||||
onCreateBranch: (name: string) => Promise<void>;
|
||||
activeIdentityProfile: GitIdentityProfile | null;
|
||||
availableIdentities: GitIdentityProfile[];
|
||||
onSelectIdentity: (profile: GitIdentityProfile) => void;
|
||||
isApplyingIdentity: boolean;
|
||||
isWorktreeMode: boolean;
|
||||
}
|
||||
|
||||
const IDENTITY_ICON_MAP: Record<
|
||||
string,
|
||||
React.ComponentType<React.ComponentProps<typeof RiGitBranchLine>>
|
||||
> = {
|
||||
branch: RiGitBranchLine,
|
||||
briefcase: RiBriefcaseLine,
|
||||
house: RiHomeLine,
|
||||
graduation: RiGraduationCapLine,
|
||||
code: RiCodeLine,
|
||||
heart: RiHeartLine,
|
||||
user: RiUser3Line,
|
||||
};
|
||||
|
||||
const IDENTITY_COLOR_MAP: Record<string, string> = {
|
||||
keyword: 'var(--syntax-keyword)',
|
||||
error: 'var(--status-error)',
|
||||
string: 'var(--syntax-string)',
|
||||
function: 'var(--syntax-function)',
|
||||
type: 'var(--syntax-type)',
|
||||
success: 'var(--status-success)',
|
||||
info: 'var(--status-info)',
|
||||
warning: 'var(--status-warning)',
|
||||
};
|
||||
|
||||
function getIdentityColor(token?: string | null) {
|
||||
if (!token) {
|
||||
return 'var(--primary)';
|
||||
}
|
||||
return IDENTITY_COLOR_MAP[token] || 'var(--primary)';
|
||||
}
|
||||
|
||||
interface IdentityIconProps {
|
||||
icon?: string | null;
|
||||
className?: string;
|
||||
colorToken?: string | null;
|
||||
}
|
||||
|
||||
const IdentityIcon: React.FC<IdentityIconProps> = ({ icon, className, colorToken }) => {
|
||||
const IconComponent = IDENTITY_ICON_MAP[icon ?? 'branch'] ?? RiUser3Line;
|
||||
return (
|
||||
<IconComponent
|
||||
className={className}
|
||||
style={{ color: getIdentityColor(colorToken) }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IdentityDropdownProps {
|
||||
activeProfile: GitIdentityProfile | null;
|
||||
identities: GitIdentityProfile[];
|
||||
onSelect: (profile: GitIdentityProfile) => void;
|
||||
isApplying: boolean;
|
||||
}
|
||||
|
||||
const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
|
||||
activeProfile,
|
||||
identities,
|
||||
onSelect,
|
||||
isApplying,
|
||||
}) => {
|
||||
const isDisabled = isApplying || identities.length === 0;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="gap-1.5 px-2 py-1 h-8 typography-ui-label"
|
||||
style={{ color: getIdentityColor(activeProfile?.color) }}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isApplying ? (
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
) : (
|
||||
<IdentityIcon
|
||||
icon={activeProfile?.icon}
|
||||
colorToken={activeProfile?.color}
|
||||
className="size-4"
|
||||
/>
|
||||
)}
|
||||
<span className="max-w-[120px] truncate hidden sm:inline">
|
||||
{activeProfile?.name || 'No identity'}
|
||||
</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-60" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="space-y-1">
|
||||
<p className="typography-ui-label text-foreground">
|
||||
{activeProfile?.userName || 'Unknown user'}
|
||||
</p>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{activeProfile?.userEmail || 'No email configured'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
{identities.length === 0 ? (
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
No profiles available to apply.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
identities.map((profile) => (
|
||||
<DropdownMenuItem key={profile.id} onSelect={() => onSelect(profile)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<IdentityIcon
|
||||
icon={profile.icon}
|
||||
colorToken={profile.color}
|
||||
className="size-4"
|
||||
/>
|
||||
<span className="flex flex-col">
|
||||
<span className="typography-ui-label text-foreground">
|
||||
{profile.name}
|
||||
</span>
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
{profile.userEmail}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
status,
|
||||
localBranches,
|
||||
remoteBranches,
|
||||
branchInfo,
|
||||
syncAction,
|
||||
onFetch,
|
||||
onPull,
|
||||
onPush,
|
||||
onCheckoutBranch,
|
||||
onCreateBranch,
|
||||
activeIdentityProfile,
|
||||
availableIdentities,
|
||||
onSelectIdentity,
|
||||
isApplyingIdentity,
|
||||
isWorktreeMode,
|
||||
}) => {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex flex-wrap items-center gap-2 border-b border-border/40 px-3 py-2 bg-background">
|
||||
{!isWorktreeMode && (
|
||||
<BranchSelector
|
||||
currentBranch={status.current}
|
||||
localBranches={localBranches}
|
||||
remoteBranches={remoteBranches}
|
||||
branchInfo={branchInfo}
|
||||
onCheckout={onCheckoutBranch}
|
||||
onCreate={onCreateBranch}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status.tracking && (
|
||||
<div className="flex items-center gap-2 px-1.5 typography-meta text-muted-foreground">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<RiArrowUpLine className="size-3.5 text-primary/70" />
|
||||
<span className="font-semibold text-foreground">{status.ahead}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<RiArrowDownLine className="size-3.5 text-primary/70" />
|
||||
<span className="font-semibold text-foreground">{status.behind}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SyncActions
|
||||
syncAction={syncAction}
|
||||
onFetch={onFetch}
|
||||
onPull={onPull}
|
||||
onPush={onPush}
|
||||
disabled={!status}
|
||||
/>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<IdentityDropdown
|
||||
activeProfile={activeIdentityProfile}
|
||||
identities={availableIdentities}
|
||||
onSelect={onSelectIdentity}
|
||||
isApplying={isApplyingIdentity}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user