Files
Project_Velocity/app/src/oracle/components/BranchBar.tsx
sayan f78655debc feat: Built the Oracle Tab1 (#14)
#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
2026-04-11 19:35:45 +05:30

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