fix(bb-ui2): integrate ThreadView into detail panels with sample data

- Wired ThreadView component into SocialDetail and SwarmDetail
- Added sample thread items for demo purposes
- Removed thread placeholders

Beads: bb-ui2.13 closed
This commit is contained in:
zenchantlive 2026-02-16 10:10:50 -08:00
parent 8dd2d01686
commit f6c5398f0c
21 changed files with 2630 additions and 58 deletions

View file

@ -7,3 +7,4 @@ export { statusGradient, statusBorder, statusDotColor, sessionStateGlow } from '
export { EpicChipStrip } from './epic-chip-strip';
export { WorkspaceHero } from './workspace-hero';
export { ProjectScopeControls } from './project-scope-controls';
export { ThreadView, type ThreadItem } from './thread-view';

View file

@ -0,0 +1,159 @@
'use client';
import { ArrowRight, Ban, CheckCircle2, MessageSquare, UserMinus } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { cn } from '@/lib/utils';
export type ThreadItemType = 'comment' | 'status_change' | 'protocol_event';
export interface ThreadItem {
id: string;
type: ThreadItemType;
author?: string;
content?: string;
from?: string;
to?: string;
event?: string;
timestamp: Date;
}
interface ThreadViewProps {
items: ThreadItem[];
onAddComment?: (text: string) => void;
}
function getInitials(name: string): string {
return name
.split(' ')
.map((part) => part[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
function formatRelativeTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
function getProtocolIcon(event?: string) {
switch (event?.toUpperCase()) {
case 'HANDOFF':
return <UserMinus className="w-4 h-4 text-amber-400" />;
case 'BLOCKED':
return <Ban className="w-4 h-4 text-rose-400" />;
case 'CLOSED':
return <CheckCircle2 className="w-4 h-4 text-emerald-400" />;
default:
return <MessageSquare className="w-4 h-4 text-text-muted" />;
}
}
function getProtocolLabel(event?: string): string {
switch (event?.toUpperCase()) {
case 'HANDOFF':
return 'Handoff';
case 'BLOCKED':
return 'Blocked';
case 'CLOSED':
return 'Closed';
default:
return 'Event';
}
}
function CommentItem({ item }: { item: ThreadItem }) {
return (
<div className="flex gap-3 py-3">
<Avatar className="h-8 w-8 flex-shrink-0">
<AvatarImage src={undefined} alt={item.author} />
<AvatarFallback className="bg-surface-muted text-text-body text-xs font-semibold">
{item.author ? getInitials(item.author) : '??'}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-text-primary text-sm font-medium">
{item.author || 'Unknown'}
</span>
<span className="text-text-muted text-xs">
{formatRelativeTime(item.timestamp)}
</span>
</div>
<p className="text-text-secondary text-sm whitespace-pre-wrap break-words">
{item.content}
</p>
</div>
</div>
);
}
function StatusChangeItem({ item }: { item: ThreadItem }) {
return (
<div className="flex items-center gap-3 py-2 text-sm">
<ArrowRight className="w-4 h-4 text-text-muted flex-shrink-0" />
<span className="text-text-muted">
Status: <span className="text-text-primary font-medium">{item.from || 'unknown'}</span>
<ArrowRight className="w-3 h-3 inline mx-1 text-text-muted" />
<span className="text-text-primary font-medium">{item.to || 'unknown'}</span>
</span>
<span className="text-text-muted text-xs ml-auto">
{formatRelativeTime(item.timestamp)}
</span>
</div>
);
}
function ProtocolEventItem({ item }: { item: ThreadItem }) {
return (
<div className="flex items-center gap-3 py-2">
<div className="flex-shrink-0">{getProtocolIcon(item.event)}</div>
<div className="flex-1 min-w-0">
<span className="text-text-primary text-sm font-medium">
{getProtocolLabel(item.event)}
</span>
{item.content && (
<span className="text-text-secondary text-sm ml-2">{item.content}</span>
)}
</div>
<span className="text-text-muted text-xs">
{formatRelativeTime(item.timestamp)}
</span>
</div>
);
}
export function ThreadView({ items, onAddComment }: ThreadViewProps) {
return (
<div className="space-y-1">
{items.length === 0 ? (
<p className="text-text-muted text-sm italic py-4">No activity yet</p>
) : (
<div className="divide-y divide-white/5">
{items.map((item) => {
switch (item.type) {
case 'comment':
return <CommentItem key={item.id} item={item} />;
case 'status_change':
return <StatusChangeItem key={item.id} item={item} />;
case 'protocol_event':
return <ProtocolEventItem key={item.id} item={item} />;
default:
return null;
}
})}
</div>
)}
</div>
);
}

View file

@ -3,8 +3,34 @@
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
import { StatusBadge } from '../shared/status-badge';
import { AgentAvatar } from '../shared/agent-avatar';
import { ThreadView, type ThreadItem } from '../shared/thread-view';
import { Plus } from 'lucide-react';
// Sample data for demo - remove when real data connected
const SAMPLE_THREAD_ITEMS: ThreadItem[] = [
{
id: '1',
type: 'status_change',
from: 'backlog',
to: 'in_progress',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
},
{
id: '2',
type: 'comment',
author: 'zenchantlive',
content: 'Started working on this task. Will need input from the API team.',
timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000),
},
{
id: '3',
type: 'protocol_event',
event: 'HANDOFF',
content: 'Handed off to bb-agent-1 for implementation',
timestamp: new Date(Date.now() - 30 * 60 * 1000),
},
];
interface SocialDetailProps {
data: SocialCardData;
}
@ -37,13 +63,11 @@ export function SocialDetail({ data }: SocialDetailProps) {
<StatusBadge status={data.status} size="sm" />
</div>
<div className="space-y-1">
<div className="space-y-2">
<h3 className="text-text-muted text-xs font-semibold uppercase tracking-wider">
Thread
</h3>
<p className="text-text-muted text-sm italic">
Thread placeholder (bb-ui2.13)
</p>
<ThreadView items={SAMPLE_THREAD_ITEMS} />
</div>
{data.blocks.length > 0 && (

View file

@ -3,8 +3,9 @@
import type { SwarmCard as SwarmCardType } from '../../lib/swarm-cards';
import { Badge } from '../../../components/ui/badge';
import { AgentAvatar } from '../shared/agent-avatar';
import { ThreadView, type ThreadItem } from '../shared/thread-view';
import { cn } from '../../lib/utils';
import { AlertTriangle, Clock, MessageSquare, Users } from 'lucide-react';
import { AlertTriangle, Clock, Users } from 'lucide-react';
interface SwarmDetailProps {
card: SwarmCardType;
@ -148,21 +149,38 @@ function LastActivitySection({ date }: { date: Date }) {
);
}
function ThreadPlaceholder() {
// Sample data for demo - remove when real data connected
const SAMPLE_SWARM_THREAD: ThreadItem[] = [
{
id: '1',
type: 'status_change',
from: 'planning',
to: 'in_progress',
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
},
{
id: '2',
type: 'comment',
author: 'bb-agent-1',
content: 'Starting work on the first batch of tasks.',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
},
{
id: '3',
type: 'protocol_event',
event: 'CLOSED',
content: 'Task bb-buff.1 completed',
timestamp: new Date(Date.now() - 30 * 60 * 1000),
},
];
function ThreadSection() {
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<MessageSquare className="h-3.5 w-3.5" style={{ color: 'var(--color-text-muted)' }} />
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
Thread
</span>
</div>
<div
className="p-3 rounded-md text-center text-xs"
style={{ backgroundColor: 'var(--color-bg-elevated)', color: 'var(--color-text-muted)' }}
>
Thread coming soon
</div>
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
Thread
</span>
<ThreadView items={SAMPLE_SWARM_THREAD} />
</div>
);
}
@ -200,8 +218,8 @@ export function SwarmDetail({ card }: SwarmDetailProps) {
{/* Last Activity */}
<LastActivitySection date={card.lastActivity} />
{/* Thread Placeholder */}
<ThreadPlaceholder />
{/* Thread */}
<ThreadSection />
</div>
);
}

197
src/video/Main.tsx Normal file
View file

@ -0,0 +1,197 @@
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence, Img, staticFile, spring } from 'remotion';
import React from 'react';
import { loadFont } from "@remotion/google-fonts/Inter";
import { Background } from './components/Background';
import { TerminalScene } from './components/TerminalScene';
import { TimelineScene } from './components/TimelineScene';
loadFont();
const COLORS = {
textPrimary: '#FFFFFF',
textSecondary: '#B8B8B8',
accentGreen: '#7CB97A',
accentTeal: '#5BA8A0',
accentAmber: '#D4A574',
};
const Logo: React.FC<{ scale?: number }> = ({ scale = 1 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const dots = [
{ color: COLORS.accentGreen, delay: 0 },
{ color: COLORS.accentTeal, delay: 5 },
{ color: COLORS.accentAmber, delay: 10 },
{ color: COLORS.accentTeal, delay: 15 },
{ color: COLORS.accentGreen, delay: 20 },
{ color: COLORS.accentAmber, delay: 25 },
{ color: COLORS.accentGreen, delay: 30 },
{ color: COLORS.accentTeal, delay: 35 },
{ color: COLORS.accentAmber, delay: 40 },
];
return (
<div className="grid grid-cols-3 gap-3 p-4" style={{ transform: `scale(${scale})` }}>
{dots.map((dot, i) => {
const spr = spring({
frame: frame - dot.delay,
fps,
config: { damping: 10 }
});
const s = interpolate(spr, [0, 1], [0, 1]);
const opacity = interpolate(spr, [0, 1], [0, 1]);
return (
<div
key={i}
className="w-4 h-4 rounded-full shadow-[0_0_15px_rgba(255,255,255,0.3)]"
style={{
backgroundColor: dot.color,
opacity,
transform: `scale(${s})`,
}}
/>
);
})}
</div>
);
};
const GlassCard: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className }) => {
return (
<div
className={`relative overflow-hidden rounded-xl border border-[rgba(255,255,255,0.1)] bg-[#363636]/40 shadow-2xl ${className}`}
style={{
backdropFilter: 'blur(16px)',
boxShadow: '0 24px 56px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.15)',
}}
>
{children}
</div>
);
};
const TitleScene: React.FC = () => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1]);
const y = interpolate(frame, [0, 20], [30, 0]);
const blur = interpolate(frame, [0, 20], [10, 0]);
return (
<AbsoluteFill className="items-center justify-center flex-col z-10">
<div style={{ opacity, transform: `translateY(${y}px)`, filter: `blur(${blur}px)` }} className="flex flex-col items-center gap-8">
<Logo scale={3} />
<div className="text-center mt-10">
<h1 className="text-8xl font-bold tracking-tight mb-6 drop-shadow-2xl" style={{ color: COLORS.textPrimary, fontFamily: 'Inter' }}>
Beadboard
</h1>
<p className="text-3xl font-medium tracking-wide drop-shadow-md" style={{ color: COLORS.textSecondary, fontFamily: 'Inter' }}>
Agent-Driven Project Orchestration
</p>
</div>
</div>
</AbsoluteFill>
);
};
const ShowcaseScene: React.FC<{ src: string; title: string; subtitle: string }> = ({ src, title, subtitle }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const spr = spring({
frame,
fps,
config: { damping: 14, mass: 0.8 },
});
const opacity = interpolate(frame, [0, 15], [0, 1]);
const scale = interpolate(spr, [0, 1], [0.92, 1]);
const y = interpolate(spr, [0, 1], [60, 0]);
// Continuous floating animation
const floatY = Math.sin(frame / 40) * 8;
return (
<AbsoluteFill className="items-center justify-center p-20 z-10">
<div className="w-full max-w-6xl flex flex-col items-center gap-10" style={{ opacity, transform: `translateY(${y}px)` }}>
<div className="text-center drop-shadow-lg">
<h2 className="text-6xl font-bold mb-4" style={{ color: COLORS.textPrimary, fontFamily: 'Inter' }}>{title}</h2>
<p className="text-2xl font-medium" style={{ color: COLORS.textSecondary, fontFamily: 'Inter' }}>{subtitle}</p>
</div>
<div style={{ transform: `translateY(${floatY}px)` }} className="w-full">
<GlassCard className="w-full aspect-video flex items-center justify-center group">
<div style={{ transform: `scale(${scale})`, width: '100%', height: '100%' }}>
<Img src={staticFile(src)} className="w-full h-full object-cover opacity-90" />
{/* Shine effect */}
<div
className="absolute inset-0 bg-gradient-to-tr from-white/0 via-white/5 to-white/0"
style={{ transform: `translateX(${Math.sin(frame / 60) * 10}%)` }}
/>
</div>
</GlassCard>
</div>
</div>
</AbsoluteFill>
);
};
const OutroScene: React.FC = () => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1]);
const scale = interpolate(frame, [0, 100], [1, 1.1]);
return (
<AbsoluteFill className="items-center justify-center z-10">
<div style={{ opacity, transform: `scale(${scale})` }} className="text-center">
<h2 className="text-8xl font-bold mb-8 drop-shadow-2xl" style={{ color: COLORS.textPrimary, fontFamily: 'Inter' }}>
Build Faster.
</h2>
<p className="text-4xl font-semibold drop-shadow-lg" style={{ color: COLORS.accentTeal, fontFamily: 'Inter' }}>
Deploy with Confidence.
</p>
</div>
</AbsoluteFill>
);
}
export const Main: React.FC = () => {
return (
<AbsoluteFill>
<Background />
<Sequence durationInFrames={90}>
<TitleScene />
</Sequence>
<Sequence from={90} durationInFrames={100}>
<ShowcaseScene
src="graph-hero.png"
title="Visual Workflow"
subtitle="Orchestrate complex agent behaviors with intuitive graphs."
/>
</Sequence>
<Sequence from={190} durationInFrames={100}>
<ShowcaseScene
src="kanban-hero.png"
title="Agent Kanban"
subtitle="Track autonomous tasks and parallel execution."
/>
</Sequence>
<Sequence from={290} durationInFrames={300}>
<TerminalScene />
</Sequence>
<Sequence from={590} durationInFrames={140}>
<TimelineScene />
</Sequence>
<Sequence from={730} durationInFrames={80}>
<OutroScene />
</Sequence>
</AbsoluteFill>
);
};

25
src/video/Root.tsx Normal file
View file

@ -0,0 +1,25 @@
import { Composition, Still } from 'remotion';
import { Main } from './Main';
import './style.css';
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="Main"
component={Main}
durationInFrames={810}
fps={30}
width={1920}
height={1080}
/>
<Still
id="Thumbnail"
component={Main}
width={1920}
height={1080}
defaultProps={{}}
/>
</>
);
};

View file

@ -0,0 +1,100 @@
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill } from 'remotion';
import React, { useMemo } from 'react';
const COLORS = {
bgBase: '#2D2D2D',
accentGreen: '#7CB97A',
accentAmber: '#D4A574',
accentTeal: '#5BA8A0',
};
const AnimatedGradient: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
// Create smooth looping motion for the blobs
const offset1 = Math.sin(frame / 60) * 10;
const offset2 = Math.cos(frame / 50) * 10;
const scale1 = interpolate(Math.sin(frame / 80), [-1, 1], [0.8, 1.2]);
const scale2 = interpolate(Math.cos(frame / 70), [-1, 1], [0.8, 1.2]);
return (
<AbsoluteFill style={{ backgroundColor: COLORS.bgBase, overflow: 'hidden' }}>
<div
className="absolute top-[-20%] left-[-10%] w-[60%] h-[60%] rounded-full opacity-20 blur-[140px]"
style={{
background: COLORS.accentGreen,
transform: `translate(${offset1}%, ${offset2}%) scale(${scale1})`,
}}
/>
<div
className="absolute bottom-[-20%] right-[-10%] w-[60%] h-[60%] rounded-full opacity-20 blur-[140px]"
style={{
background: COLORS.accentAmber,
transform: `translate(${-offset2}%, ${-offset1}%) scale(${scale2})`,
}}
/>
<div
className="absolute top-[30%] left-[30%] w-[40%] h-[40%] rounded-full opacity-10 blur-[120px]"
style={{
background: COLORS.accentTeal,
transform: `translate(${offset2 * 0.5}%, ${offset1 * 0.5}%) scale(${scale1})`,
}}
/>
</AbsoluteFill>
);
};
const DotGrid: React.FC = () => {
const frame = useCurrentFrame();
const { width, height } = useVideoConfig();
// Generate a static grid of dots
// Only calculate once
const dots = useMemo(() => {
const d = [];
const spacing = 80;
const cols = Math.ceil(width / spacing);
const rows = Math.ceil(height / spacing);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
d.push({ x: i * spacing, y: j * spacing, delay: (i + j) * 2 });
}
}
return d;
}, [width, height]);
return (
<AbsoluteFill className="items-center justify-center">
<svg width="100%" height="100%">
{dots.map((dot, i) => {
// Subtle fade in/out ripple effect based on position
const wave = Math.sin((frame - dot.delay) / 20);
const opacity = interpolate(wave, [-1, 1], [0.03, 0.15]);
const scale = interpolate(wave, [-1, 1], [0.5, 1.2]);
return (
<circle
key={i}
cx={dot.x + 40}
cy={dot.y + 40}
r={2 * scale}
fill="white"
opacity={opacity}
/>
);
})}
</svg>
</AbsoluteFill>
);
};
export const Background: React.FC = () => {
return (
<AbsoluteFill>
<AnimatedGradient />
<DotGrid />
</AbsoluteFill>
);
};

View file

@ -0,0 +1,120 @@
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence, spring } from 'remotion';
import React, { useMemo } from 'react';
const TerminalLine: React.FC<{ text: string; delay: number; color?: string }> = ({ text, delay, color = '#d1d5db' }) => {
const frame = useCurrentFrame();
const chars = text.split('');
return (
<div className="font-mono text-xl mb-2 flex">
{chars.map((char, i) => {
const show = frame > delay + i * 1.5;
return (
<span key={i} style={{ opacity: show ? 1 : 0, color }}>
{char}
</span>
);
})}
</div>
);
};
const JSONLine: React.FC<{ data: object; delay: number }> = ({ data, delay }) => {
const frame = useCurrentFrame();
const str = JSON.stringify(data, null, 2);
const lines = str.split('\n');
const show = frame > delay;
const opacity = interpolate(frame, [delay, delay + 10], [0, 1]);
const y = interpolate(frame, [delay, delay + 10], [10, 0]);
if (!show) return null;
return (
<div style={{ opacity, transform: `translateY(${y}px)` }} className="font-mono text-sm text-green-400/90 bg-black/20 p-4 rounded-md border border-green-500/20 my-2">
{lines.map((line, i) => (
<div key={i}>{line}</div>
))}
</div>
);
}
export const TerminalScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 15], [0, 1]);
const scale = interpolate(frame, [0, 15], [0.95, 1]);
// Header animation
const headerY = interpolate(frame, [0, 20], [20, 0]);
const headerOpacity = interpolate(frame, [0, 20], [0, 1]);
return (
<AbsoluteFill className="items-center justify-center bg-transparent p-20 z-10">
{/* Header */}
<div style={{ transform: `translateY(${headerY}px)`, opacity: headerOpacity }} className="absolute top-20 text-center w-full">
<h2 className="text-6xl font-bold text-white mb-2 font-['Inter'] drop-shadow-lg">Protocol v1</h2>
<p className="text-xl text-teal-400 font-mono tracking-widest uppercase">Safe Coordination Contract</p>
</div>
<div
className="w-full max-w-5xl bg-[#1e1e1e] rounded-xl overflow-hidden shadow-2xl border border-gray-700/50"
style={{ opacity, transform: `scale(${scale})` }}
>
{/* Terminal Header */}
<div className="bg-[#2d2d2d] px-4 py-3 flex items-center gap-2 border-b border-gray-700">
<div className="w-3 h-3 rounded-full bg-red-500" />
<div className="w-3 h-3 rounded-full bg-yellow-500" />
<div className="w-3 h-3 rounded-full bg-green-500" />
<div className="ml-4 text-xs text-gray-400 font-mono">beadboard-agent -zsh 80x24</div>
</div>
{/* Terminal Body */}
<div className="p-6 h-[600px] font-mono text-gray-300 overflow-hidden relative">
<Sequence from={20}>
<TerminalLine text="> bb agent heartbeat --agent amber-otter --json" delay={0} color="#a5f3fc" />
</Sequence>
<Sequence from={60}>
<JSONLine
delay={0}
data={{
status: "ok",
agent_id: "amber-otter",
last_seen: "2026-02-16T10:42:15Z",
liveness: "active"
}}
/>
</Sequence>
<Sequence from={110}>
<TerminalLine text="> bb protocol emit HANDOFF --to cobalt-harbor" delay={0} color="#a5f3fc" />
</Sequence>
<Sequence from={150}>
<div className="mt-4 p-4 border-l-4 border-blue-500 bg-blue-500/10">
<TerminalLine text="[EVENT] HANDOFF DETECTED" delay={0} color="#60a5fa" />
<TerminalLine text="Scope: src/components/sessions/*" delay={10} />
<TerminalLine text="From: amber-otter -> To: cobalt-harbor" delay={20} />
<TerminalLine text="Reason: Implementation complete, ready for review." delay={30} />
</div>
</Sequence>
<Sequence from={250}>
<div className="mt-4 p-4 border-l-4 border-yellow-500 bg-yellow-500/10">
<TerminalLine text="[WARN] INCURSION PREVENTED" delay={0} color="#fbbf24" />
<TerminalLine text="Target: src/lib/parser.ts (Locked by: obsidian-fox)" delay={10} />
<TerminalLine text="Action: Write blocked. Queueing request." delay={20} />
</div>
</Sequence>
{/* Scanlines / CRT Effect Overlay */}
<div className="absolute inset-0 pointer-events-none opacity-5 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))]" style={{ backgroundSize: "100% 2px, 3px 100%" }} />
</div>
</div>
</AbsoluteFill>
);
};

View file

@ -0,0 +1,112 @@
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence, spring } from 'remotion';
import React from 'react';
const COLORS = {
bgBase: '#2D2D2D',
cardBg: '#363636',
accentGreen: '#7CB97A',
accentAmber: '#D4A574',
accentTeal: '#5BA8A0',
textPrimary: '#FFFFFF',
textSecondary: '#B8B8B8',
border: 'rgba(255, 255, 255, 0.08)',
};
const TimelineCard: React.FC<{
title: string;
subtitle: string;
time: string;
type: 'commit' | 'issue' | 'alert';
index: number;
}> = ({ title, subtitle, time, type, index }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delay = index * 5;
const spr = spring({
frame: frame - delay,
fps,
config: { damping: 14, mass: 0.8 },
});
const y = interpolate(spr, [0, 1], [50, 0]);
const opacity = interpolate(spr, [0, 1], [0, 1]);
let iconColor = COLORS.textSecondary;
if (type === 'commit') iconColor = COLORS.accentTeal;
if (type === 'issue') iconColor = COLORS.accentGreen;
if (type === 'alert') iconColor = COLORS.accentAmber;
return (
<div
style={{ opacity, transform: `translateY(${y}px)` }}
className="flex items-start gap-4 p-4 rounded-lg border bg-[#363636] shadow-lg mb-4 w-full max-w-2xl"
// className="flex items-start gap-4 p-4 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#363636] shadow-lg mb-4 w-full max-w-2xl"
>
<div className="mt-1 w-3 h-3 rounded-full" style={{ backgroundColor: iconColor }} />
<div className="flex-1">
<div className="flex justify-between items-baseline">
<h3 className="text-lg font-semibold text-white">{title}</h3>
<span className="text-xs text-gray-500 font-mono">{time}</span>
</div>
<p className="text-sm text-gray-400 mt-1">{subtitle}</p>
</div>
</div>
);
};
export const TimelineScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const titleOpacity = interpolate(frame, [0, 20], [0, 1]);
const titleY = interpolate(frame, [0, 20], [20, 0]);
return (
<AbsoluteFill className="items-center justify-center p-10 z-10 flex-col">
<div style={{ opacity: titleOpacity, transform: `translateY(${titleY}px)` }} className="mb-12 text-center">
<h2 className="text-6xl font-bold text-white mb-2 font-['Inter'] drop-shadow-lg">Live Activity Feed</h2>
<p className="text-xl text-teal-400 font-mono tracking-widest uppercase">Real-time Project Pulse</p>
</div>
<div className="flex flex-col w-full max-w-2xl">
<div className="text-xs font-mono text-gray-500 mb-4 uppercase tracking-wider ml-2">Today</div>
<Sequence from={10}>
<TimelineCard
index={0} type="commit" title="feat: Implement Session Protocol v1"
subtitle="amber-otter pushed to main" time="10:42 AM"
/>
</Sequence>
<Sequence from={25}>
<TimelineCard
index={1} type="issue" title="Docs: Update RFC-001"
subtitle="cobalt-harbor commented on #23" time="10:45 AM"
/>
</Sequence>
<Sequence from={40}>
<TimelineCard
index={2} type="alert" title="Incursion Alert"
subtitle="obsidian-fox attempted write to locked scope" time="11:02 AM"
/>
</Sequence>
<Sequence from={60}>
<div className="text-xs font-mono text-gray-500 mt-6 mb-4 uppercase tracking-wider ml-2">Yesterday</div>
</Sequence>
<Sequence from={70}>
<TimelineCard
index={3} type="issue" title="Refactor: Agent Registry"
subtitle="emerald-wolf closed issue #19" time="4:20 PM"
/>
</Sequence>
<Sequence from={85}>
<TimelineCard
index={4} type="commit" title="fix: Graph layout rendering"
subtitle="amber-otter pushed to feature/graph-v2" time="3:15 PM"
/>
</Sequence>
</div>
</AbsoluteFill>
);
};

4
src/video/index.ts Normal file
View file

@ -0,0 +1,4 @@
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);

3
src/video/style.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;