forked from sagnik/Project_Velocity
#13 Built the complete Oracle Tab with all the functionalities. Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#14
202 lines
7.3 KiB
TypeScript
202 lines
7.3 KiB
TypeScript
/**
|
|
* BranchBar — Top-of-page branch status bar
|
|
* Shows: page title, branch identity badge, revision number, execution status,
|
|
* share action, merge request indicator, rollback affordance.
|
|
* Must remain visible at all times (sticky).
|
|
*/
|
|
import { useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
GitBranch, GitMerge, Share2, RotateCcw, Dot, Users,
|
|
CheckCircle2, Clock, AlertCircle, Loader2, GitFork
|
|
} from 'lucide-react';
|
|
import type { CanvasPage, PromptExecution, MergeRequest } from '../types/canvas';
|
|
|
|
interface BranchBarProps {
|
|
page: CanvasPage | null;
|
|
inFlightExecution: PromptExecution | null;
|
|
mergeRequests?: MergeRequest[];
|
|
isConnected: boolean;
|
|
onShare: () => void;
|
|
onRollback: () => void;
|
|
onOpenMergeReview: () => void;
|
|
}
|
|
|
|
const STATUS_CONFIG = {
|
|
received: { icon: Clock, color: '#94a3b8', label: 'Received' },
|
|
planning: { icon: Loader2, color: '#60a5fa', label: 'Planning…', spin: true },
|
|
validated: { icon: CheckCircle2, color: '#34d399', label: 'Validated' },
|
|
executing: { icon: Loader2, color: '#a78bfa', label: 'Executing…', spin: true },
|
|
completed: { icon: CheckCircle2, color: '#34d399', label: 'Completed' },
|
|
failed: { icon: AlertCircle, color: '#f87171', label: 'Failed' },
|
|
clarification_required: { icon: AlertCircle, color: '#fbbf24', label: 'Clarification needed' },
|
|
} as const;
|
|
|
|
export function BranchBar({
|
|
page,
|
|
inFlightExecution,
|
|
mergeRequests = [],
|
|
isConnected,
|
|
onShare,
|
|
onRollback,
|
|
onOpenMergeReview,
|
|
}: BranchBarProps) {
|
|
const [shareHovered, setShareHovered] = useState(false);
|
|
const openMRCount = mergeRequests.filter((mr) => mr.status === 'open').length;
|
|
const isFork = page?.pageType === 'fork';
|
|
|
|
const executionStatus = inFlightExecution?.status;
|
|
const statusCfg = executionStatus ? STATUS_CONFIG[executionStatus] : null;
|
|
const StatusIcon = statusCfg?.icon;
|
|
|
|
return (
|
|
<div
|
|
className="relative z-20 px-4 pt-3 pb-2 flex-shrink-0"
|
|
style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}
|
|
>
|
|
<div
|
|
className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-2xl"
|
|
style={{
|
|
background: 'rgba(10, 11, 18, 0.85)',
|
|
border: '1px solid rgba(255,255,255,0.08)',
|
|
backdropFilter: 'blur(24px)',
|
|
WebkitBackdropFilter: 'blur(24px)',
|
|
}}
|
|
>
|
|
{/* Left: Page title + branch identity */}
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{isFork ? (
|
|
<GitFork className="w-4 h-4 text-violet-400" />
|
|
) : (
|
|
<GitBranch className="w-4 h-4 text-blue-400" />
|
|
)}
|
|
<span
|
|
className="text-xs font-semibold px-2 py-0.5 rounded-full flex-shrink-0"
|
|
style={{
|
|
background: isFork ? 'rgba(139,92,246,0.15)' : 'rgba(59,130,246,0.15)',
|
|
color: isFork ? '#c4b5fd' : '#93c5fd',
|
|
border: `1px solid ${isFork ? 'rgba(139,92,246,0.3)' : 'rgba(59,130,246,0.3)'}`,
|
|
}}
|
|
>
|
|
{page?.branchName ?? 'main'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
<span className="text-sm font-medium text-zinc-200 truncate">
|
|
{page?.title ?? 'Oracle Canvas'}
|
|
</span>
|
|
{page && (
|
|
<span className="text-xs text-zinc-600 flex-shrink-0 font-mono">
|
|
rev.{page.headRevision}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Center: execution status */}
|
|
<AnimatePresence mode="wait">
|
|
{statusCfg && StatusIcon && (
|
|
<motion.div
|
|
key={executionStatus}
|
|
initial={{ opacity: 0, scale: 0.85 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.85 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="flex items-center gap-1.5 flex-shrink-0"
|
|
>
|
|
<StatusIcon
|
|
className="w-3.5 h-3.5"
|
|
style={{
|
|
color: statusCfg.color,
|
|
// @ts-expect-error spin is a custom property
|
|
animation: statusCfg.spin ? 'spin 1s linear infinite' : undefined,
|
|
}}
|
|
/>
|
|
<span className="text-xs font-medium" style={{ color: statusCfg.color }}>
|
|
{statusCfg.label}
|
|
</span>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Right: actions */}
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{/* Presence indicator */}
|
|
{page && page.presence.activeViewers > 1 && (
|
|
<div className="flex items-center gap-1 text-xs text-zinc-500">
|
|
<Users className="w-3.5 h-3.5" />
|
|
<span>{page.presence.activeViewers}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Connection dot */}
|
|
<div className="flex items-center gap-1">
|
|
<Dot
|
|
className="w-5 h-5 -mx-1.5"
|
|
style={{ color: isConnected ? '#34d399' : '#6b7280' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Rollback */}
|
|
<button
|
|
onClick={onRollback}
|
|
title="View revision history and rollback"
|
|
className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg transition-all"
|
|
style={{
|
|
color: '#71717a',
|
|
background: 'transparent',
|
|
border: '1px solid rgba(255,255,255,0.07)',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.05)';
|
|
e.currentTarget.style.color = '#a1a1aa';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'transparent';
|
|
e.currentTarget.style.color = '#71717a';
|
|
}}
|
|
>
|
|
<RotateCcw className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">History</span>
|
|
</button>
|
|
|
|
{/* Merge Requests */}
|
|
{openMRCount > 0 && (
|
|
<button
|
|
onClick={onOpenMergeReview}
|
|
className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg transition-all"
|
|
style={{
|
|
background: 'rgba(251,191,36,0.12)',
|
|
color: '#fbbf24',
|
|
border: '1px solid rgba(251,191,36,0.25)',
|
|
}}
|
|
>
|
|
<GitMerge className="w-3.5 h-3.5" />
|
|
<span>{openMRCount} open</span>
|
|
</button>
|
|
)}
|
|
|
|
{/* Share */}
|
|
<motion.button
|
|
onClick={onShare}
|
|
onMouseEnter={() => setShareHovered(true)}
|
|
onMouseLeave={() => setShareHovered(false)}
|
|
className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg font-medium transition-all"
|
|
style={{
|
|
background: shareHovered ? 'rgba(59,130,246,0.25)' : 'rgba(59,130,246,0.15)',
|
|
color: '#93c5fd',
|
|
border: '1px solid rgba(59,130,246,0.3)',
|
|
}}
|
|
whileTap={{ scale: 0.96 }}
|
|
>
|
|
<Share2 className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Share</span>
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|