refactor: restructure repo for cleaner GitHub landing page

Reduce visible top-level items from 21 to 14:
- Untrack test-results/, tmp/, public symlink (added to .gitignore)
- Move agent-teams/ → docs/agent-teams/
- Move mobile-test/ → test/mobile/
- Move tools/remotion/ → scripts/remotion/
- Move eslint.config.js, vitest.config.ts → config/

All path references updated across CLAUDE.md, package.json,
.prettierignore, vitest configs, and capture scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-04-03 16:23:54 +02:00
co-authored by Claude Opus 4.6
parent 5078f5251d
commit 7101e64800
82 changed files with 53 additions and 46 deletions
+1 -1
View File
@@ -428,7 +428,7 @@ const SUBAGENT_ACTIVITY = {
'agent-002': [
{ type: 'tool', tool: 'Glob', input: { pattern: 'test/**/*.test.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/test/respawn-test-utils.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/vitest.config.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/config/vitest.config.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'message', role: 'assistant', text: 'Analyzing test patterns: MockSession, unique ports, fileParallelism: false...', timestamp: new Date().toISOString(), agentId: 'agent-002' },
],
};
+14 -14
View File
@@ -9,7 +9,7 @@
*
* Usage: node scripts/capture-video-screenshots.mjs
* Port: 3198 (static file server)
* Output: remotion/public/ (6 PNGs)
* Output: scripts/scripts/remotion/public/ (6 PNGs)
*/
import { chromium } from 'playwright';
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const PROJECT_ROOT = join(__dirname, '..');
const PUBLIC_DIR = join(PROJECT_ROOT, 'src', 'web', 'public');
const OUTPUT_DIR = join(PROJECT_ROOT, 'remotion', 'public');
const OUTPUT_DIR = join(PROJECT_ROOT, 'scripts', 'remotion', 'public');
const PORT = 3198;
const DESKTOP_VIEWPORT = { width: 1920, height: 1080 };
@@ -516,7 +516,7 @@ async function captureDesktopWelcome(browser) {
path: join(OUTPUT_DIR, 'desktop-welcome.png'),
fullPage: false,
});
console.log(' Saved: remotion/public/desktop-welcome.png');
console.log(' Saved: scripts/remotion/public/desktop-welcome.png');
} finally {
await context.close();
}
@@ -545,7 +545,7 @@ async function captureDesktopClaude(browser) {
path: join(OUTPUT_DIR, 'desktop-claude.png'),
fullPage: false,
});
console.log(' Saved: remotion/public/desktop-claude.png');
console.log(' Saved: scripts/remotion/public/desktop-claude.png');
} finally {
await context.close();
}
@@ -575,7 +575,7 @@ async function captureDesktopBothClaude(browser) {
path: join(OUTPUT_DIR, 'desktop-both-claude.png'),
fullPage: false,
});
console.log(' Saved: remotion/public/desktop-both-claude.png');
console.log(' Saved: scripts/remotion/public/desktop-both-claude.png');
} finally {
await context.close();
}
@@ -605,7 +605,7 @@ async function captureDesktopBothOpencode(browser) {
path: join(OUTPUT_DIR, 'desktop-both-opencode.png'),
fullPage: false,
});
console.log(' Saved: remotion/public/desktop-both-opencode.png');
console.log(' Saved: scripts/remotion/public/desktop-both-opencode.png');
} finally {
await context.close();
}
@@ -634,7 +634,7 @@ async function captureMobileClaude(browser) {
path: join(OUTPUT_DIR, 'mobile-claude.png'),
fullPage: false,
});
console.log(' Saved: remotion/public/mobile-claude.png');
console.log(' Saved: scripts/remotion/public/mobile-claude.png');
} finally {
await context.close();
}
@@ -663,7 +663,7 @@ async function captureMobileOpencode(browser) {
path: join(OUTPUT_DIR, 'mobile-opencode.png'),
fullPage: false,
});
console.log(' Saved: remotion/public/mobile-opencode.png');
console.log(' Saved: scripts/remotion/public/mobile-opencode.png');
} finally {
await context.close();
}
@@ -706,12 +706,12 @@ async function main() {
console.log('All 6 screenshots captured!');
console.log('='.repeat(60));
console.log('\nOutput files:');
console.log(' remotion/public/desktop-welcome.png');
console.log(' remotion/public/desktop-claude.png');
console.log(' remotion/public/desktop-both-claude.png');
console.log(' remotion/public/desktop-both-opencode.png');
console.log(' remotion/public/mobile-claude.png');
console.log(' remotion/public/mobile-opencode.png');
console.log(' scripts/remotion/public/desktop-welcome.png');
console.log(' scripts/remotion/public/desktop-claude.png');
console.log(' scripts/remotion/public/desktop-both-claude.png');
console.log(' scripts/remotion/public/desktop-both-opencode.png');
console.log(' scripts/remotion/public/mobile-claude.png');
console.log(' scripts/remotion/public/mobile-opencode.png');
} catch (err) {
console.error('\nFatal error:', err.message);
console.error(err.stack);
+27
View File
@@ -0,0 +1,27 @@
import React from 'react';
import { Composition } from 'remotion';
import { CodemanDemo, TOTAL_FRAMES } from './compositions/CodemanDemo';
import { ZerolagDemo, ZEROLAG_TOTAL_FRAMES } from './compositions/ZerolagDemo';
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="CodemanDemo"
component={CodemanDemo}
durationInFrames={TOTAL_FRAMES}
fps={30}
width={1920}
height={1080}
/>
<Composition
id="ZerolagDemo"
component={ZerolagDemo}
durationInFrames={ZEROLAG_TOTAL_FRAMES}
fps={30}
width={1920}
height={1080}
/>
</>
);
};
+108
View File
@@ -0,0 +1,108 @@
import React from 'react';
import {
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
type ClickCursorProps = {
/** X position to move to */
x: number;
/** Y position to move to */
y: number;
/** Frame at which cursor starts moving (local frame) */
moveStart: number;
/** Frame at which click happens */
clickAt: number;
/** Starting X (defaults to center-right) */
fromX?: number;
/** Starting Y */
fromY?: number;
};
export const ClickCursor: React.FC<ClickCursorProps> = ({
x,
y,
moveStart,
clickAt,
fromX = 960,
fromY = 400,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Move animation (spring-based)
const moveProgress = spring({
frame: Math.max(0, frame - moveStart),
fps,
config: { damping: 200 },
durationInFrames: Math.max(1, clickAt - moveStart),
});
const cursorX = interpolate(moveProgress, [0, 1], [fromX, x]);
const cursorY = interpolate(moveProgress, [0, 1], [fromY, y]);
// Click pulse
const clickProgress =
frame >= clickAt
? interpolate(frame - clickAt, [0, 8], [0, 1], {
extrapolateRight: 'clamp',
})
: 0;
const clickScale = interpolate(clickProgress, [0, 0.5, 1], [0, 1.2, 0], {
extrapolateRight: 'clamp',
});
const clickOpacity = interpolate(clickProgress, [0, 0.5, 1], [0, 0.6, 0], {
extrapolateRight: 'clamp',
});
// Hide cursor before it starts moving
const cursorOpacity =
frame < moveStart
? 0
: interpolate(frame - moveStart, [0, 5], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<div
style={{
position: 'absolute',
left: cursorX,
top: cursorY,
opacity: cursorOpacity,
zIndex: 100,
pointerEvents: 'none',
}}
>
{/* Click pulse ring */}
<div
style={{
position: 'absolute',
width: 30,
height: 30,
borderRadius: '50%',
border: '2px solid rgba(255,255,255,0.5)',
transform: `translate(-50%, -50%) scale(${clickScale})`,
opacity: clickOpacity,
}}
/>
{/* Cursor arrow */}
<svg
width="20"
height="24"
viewBox="0 0 20 24"
fill="none"
style={{ filter: 'drop-shadow(1px 2px 3px rgba(0,0,0,0.5))' }}
>
<path
d="M2 2L2 20L7 15L12 22L15 20L10 13L17 13L2 2Z"
fill="white"
stroke="black"
strokeWidth="1.5"
/>
</svg>
</div>
);
};
+207
View File
@@ -0,0 +1,207 @@
import React from 'react';
import { fonts } from '../lib/theme';
type IOSKeyboardProps = {
activeKey?: string;
/** How many frames since the key was pressed (for highlight decay) */
pressAge?: number;
};
const ROW_1 = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'];
const ROW_2 = ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'];
const ROW_3 = ['z', 'x', 'c', 'v', 'b', 'n', 'm'];
const KEY_H = 42;
const KEY_GAP = 6;
const ROW_GAP = 11;
const SIDE_PAD = 3;
const KEYBOARD_BG = '#1c1c1e';
const KEY_BG = '#3a3a3c';
const KEY_BG_ACTIVE = '#636366';
const KEY_TEXT = '#fff';
const SPECIAL_BG = '#2c2c2e';
const Key: React.FC<{
label: string;
width: number;
isActive: boolean;
pressAge: number;
fontSize?: number;
}> = ({ label, width, isActive, pressAge, fontSize = 22 }) => {
// Highlight decays over 4 frames
const highlightOpacity = isActive && pressAge < 4 ? 1 - pressAge / 4 : 0;
const bg = highlightOpacity > 0
? lerpColor(KEY_BG, KEY_BG_ACTIVE, highlightOpacity)
: KEY_BG;
return (
<div
style={{
width,
height: KEY_H,
borderRadius: 5,
background: bg,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize,
fontFamily: fonts.ui,
color: KEY_TEXT,
fontWeight: 300,
flexShrink: 0,
}}
>
{label}
</div>
);
};
function lerpColor(a: string, b: string, t: number): string {
const pa = parseInt(a.slice(1), 16);
const pb = parseInt(b.slice(1), 16);
const r = Math.round(((pa >> 16) & 0xff) * (1 - t) + ((pb >> 16) & 0xff) * t);
const g = Math.round(((pa >> 8) & 0xff) * (1 - t) + ((pb >> 8) & 0xff) * t);
const bl = Math.round((pa & 0xff) * (1 - t) + (pb & 0xff) * t);
return `#${((r << 16) | (g << 8) | bl).toString(16).padStart(6, '0')}`;
}
export const IOSKeyboard: React.FC<IOSKeyboardProps> = ({ activeKey, pressAge = 99 }) => {
const isActive = (key: string) =>
activeKey !== undefined && key.toLowerCase() === activeKey.toLowerCase();
const age = (key: string) => (isActive(key) ? pressAge : 99);
// Backspace/delete key highlight
const deleteHighlight = activeKey === '⌫' && pressAge < 4 ? 1 - pressAge / 4 : 0;
const deleteBg = deleteHighlight > 0 ? lerpColor(SPECIAL_BG, KEY_BG_ACTIVE, deleteHighlight) : SPECIAL_BG;
// Key widths: 10 keys + 9 gaps in ~375px row → each key ~33px
const letterKeyW = 33;
// Row 2 has 9 keys → same key width but centered with side padding
// Row 3 has shift + 7 keys + delete
return (
<div
style={{
width: '100%',
background: KEYBOARD_BG,
padding: `${ROW_GAP}px ${SIDE_PAD}px 20px`,
display: 'flex',
flexDirection: 'column',
gap: ROW_GAP,
}}
>
{/* Row 1: q-p */}
<div style={{ display: 'flex', gap: KEY_GAP, justifyContent: 'center' }}>
{ROW_1.map((k) => (
<Key key={k} label={k} width={letterKeyW} isActive={isActive(k)} pressAge={age(k)} />
))}
</div>
{/* Row 2: a-l */}
<div style={{ display: 'flex', gap: KEY_GAP, justifyContent: 'center' }}>
{ROW_2.map((k) => (
<Key key={k} label={k} width={letterKeyW} isActive={isActive(k)} pressAge={age(k)} />
))}
</div>
{/* Row 3: shift + z-m + delete */}
<div style={{ display: 'flex', gap: KEY_GAP, justifyContent: 'center' }}>
<div
style={{
width: 42,
height: KEY_H,
borderRadius: 5,
background: SPECIAL_BG,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<svg width="20" height="16" viewBox="0 0 20 16" fill="none">
<path d="M10 2L17 9H13V14H7V9H3L10 2Z" fill="#fff" />
</svg>
</div>
{ROW_3.map((k) => (
<Key key={k} label={k} width={letterKeyW} isActive={isActive(k)} pressAge={age(k)} />
))}
<div
style={{
width: 42,
height: KEY_H,
borderRadius: 5,
background: deleteBg,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<svg width="22" height="16" viewBox="0 0 22 16" fill="none">
<path d="M7 1L1 8L7 15H21V1H7Z" stroke="#fff" strokeWidth="1.5" fill="none" />
<path d="M12 5L17 10M17 5L12 10" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</div>
</div>
{/* Row 4: 123 / globe / space / return */}
<div style={{ display: 'flex', gap: KEY_GAP, justifyContent: 'center' }}>
<div
style={{
width: 42,
height: KEY_H,
borderRadius: 5,
background: SPECIAL_BG,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: 15,
fontFamily: fonts.ui,
color: '#fff',
}}
>
123
</div>
<div
style={{
width: 38,
height: KEY_H,
borderRadius: 5,
background: SPECIAL_BG,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="8" stroke="#fff" strokeWidth="1.2" />
<path d="M4 10H16M10 4C7 7 7 13 10 16M10 4C13 7 13 13 10 16" stroke="#fff" strokeWidth="1" />
</svg>
</div>
{/* Space bar */}
<Key
label="space"
width={186}
isActive={isActive(' ')}
pressAge={age(' ')}
fontSize={15}
/>
<div
style={{
width: 88,
height: KEY_H,
borderRadius: 5,
background: SPECIAL_BG,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: 15,
fontFamily: fonts.ui,
color: '#fff',
}}
>
return
</div>
</div>
</div>
);
};
@@ -0,0 +1,171 @@
import React from 'react';
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
/**
* Pixel-accurate iPhone 17 Pro frame.
*
* Dimensions based on iPhone 16 Pro (same form factor):
* - Screen: 393×852 CSS points (2622×1206 @3x)
* - Corner radius: 55px (device), 50px (screen inner)
* - Bezel: ~3.5px (thinnest in any iPhone)
* - Dynamic Island: 126×37 pill, centered 13px from top
* - Safe area: top 59px, bottom 34px (home indicator)
* - Frame: natural titanium (#8a8a8e border)
*/
type IPhone17ProFrameProps = {
children: React.ReactNode;
/** Disable the spring entrance animation */
noAnimation?: boolean;
};
// Device dimensions (CSS points)
const SCREEN_W = 393;
const SCREEN_H = 852;
const BEZEL = 4;
const DEVICE_W = SCREEN_W + BEZEL * 2; // 401
const DEVICE_H = SCREEN_H + BEZEL * 2; // 860
const DEVICE_RADIUS = 55;
const SCREEN_RADIUS = 50;
// Dynamic Island
const DI_W = 126;
const DI_H = 37;
const DI_TOP = 13; // from top of screen
const DI_RADIUS = DI_H / 2; // pill shape
export { SCREEN_W, SCREEN_H };
export const IPhone17ProFrame: React.FC<IPhone17ProFrameProps> = ({ children, noAnimation }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = noAnimation
? 1
: spring({ frame, fps, config: { damping: 15, stiffness: 80 } });
return (
<div
style={{
width: DEVICE_W,
height: DEVICE_H,
transform: `scale(${scale})`,
transformOrigin: 'center center',
position: 'relative',
}}
>
{/* Titanium frame (outer body) */}
<div
style={{
width: DEVICE_W,
height: DEVICE_H,
borderRadius: DEVICE_RADIUS,
background: '#2c2c2e', // dark titanium
border: '1.5px solid #48484a', // subtle edge highlight
boxShadow: [
'0 2px 4px rgba(0,0,0,0.3)', // close shadow
'0 12px 40px rgba(0,0,0,0.5)', // mid shadow
'0 30px 80px rgba(0,0,0,0.4)', // far shadow
'inset 0 1px 0 rgba(255,255,255,0.05)', // top edge gleam
].join(', '),
position: 'relative',
overflow: 'hidden',
}}
>
{/* Side button (right) — power */}
<div
style={{
position: 'absolute',
right: -2,
top: 180,
width: 3,
height: 65,
borderRadius: '0 2px 2px 0',
background: '#48484a',
}}
/>
{/* Side buttons (left) — volume up, down, action */}
<div
style={{
position: 'absolute',
left: -2,
top: 140,
width: 3,
height: 28,
borderRadius: '2px 0 0 2px',
background: '#48484a',
}}
/>
<div
style={{
position: 'absolute',
left: -2,
top: 185,
width: 3,
height: 50,
borderRadius: '2px 0 0 2px',
background: '#48484a',
}}
/>
<div
style={{
position: 'absolute',
left: -2,
top: 250,
width: 3,
height: 50,
borderRadius: '2px 0 0 2px',
background: '#48484a',
}}
/>
{/* Screen */}
<div
style={{
position: 'absolute',
top: BEZEL,
left: BEZEL,
width: SCREEN_W,
height: SCREEN_H,
borderRadius: SCREEN_RADIUS,
overflow: 'hidden',
background: '#000',
}}
>
{/* App content */}
{children}
{/* Dynamic Island (on top of everything) */}
<div
style={{
position: 'absolute',
top: DI_TOP,
left: (SCREEN_W - DI_W) / 2,
width: DI_W,
height: DI_H,
borderRadius: DI_RADIUS,
background: '#000',
zIndex: 50,
}}
/>
</div>
</div>
{/* Home indicator */}
<div
style={{
position: 'absolute',
bottom: BEZEL + 8,
left: '50%',
transform: 'translateX(-50%)',
width: 134,
height: 5,
borderRadius: 3,
background: 'rgba(255,255,255,0.2)',
zIndex: 60,
}}
/>
</div>
);
};
@@ -0,0 +1,85 @@
import React from 'react';
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
import { colors } from '../lib/theme';
type PhoneFrameProps = {
children: React.ReactNode;
/** Width of the phone viewport */
width?: number;
/** Height of the phone viewport */
height?: number;
};
export const PhoneFrame: React.FC<PhoneFrameProps> = ({
children,
width = 375,
height = 812,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Scale-in animation
const scale = spring({
frame,
fps,
config: { damping: 15, stiffness: 80 },
});
const bezelW = width + 24;
const bezelH = height + 24;
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
transform: `scale(${scale})`,
}}
>
<div
style={{
width: bezelW,
height: bezelH,
borderRadius: 44,
background: '#1a1a1a',
border: '2px solid #333',
padding: 12,
position: 'relative',
boxShadow: '0 20px 60px rgba(0,0,0,0.6)',
}}
>
{/* Notch / Dynamic Island */}
<div
style={{
position: 'absolute',
top: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 120,
height: 28,
borderRadius: 14,
background: '#000',
zIndex: 20,
}}
/>
{/* Screen content */}
<div
style={{
width,
height,
borderRadius: 32,
overflow: 'hidden',
background: colors.bg.dark,
position: 'relative',
}}
>
{children}
</div>
</div>
</div>
);
};
@@ -0,0 +1,117 @@
import React from 'react';
import { useCurrentFrame } from 'remotion';
import { colors, fonts } from '../lib/theme';
type OverlayChar = {
char: string;
confirmed: boolean;
};
type TerminalScreenProps = {
typed: string;
cursorVisible: boolean;
overlayChars?: OverlayChar[];
fontSize?: number;
};
/**
* Renders a terminal area styled exactly like the real Codeman xterm.js terminal.
* Background #0d0d0d, Fira Code font, block cursor, Claude Code prompt.
*/
export const TerminalScreen: React.FC<TerminalScreenProps> = ({
typed,
cursorVisible,
overlayChars,
fontSize = 28,
}) => {
const frame = useCurrentFrame();
// Block cursor — solid, no blink (matches Codeman's cursorBlink: false)
const cursorOn = cursorVisible;
// But add a subtle blink for video clarity so viewers notice it
const cursorOpacity = cursorOn ? (Math.floor(frame / 20) % 2 === 0 ? 0.9 : 0.6) : 0;
const lineHeight = Math.round(fontSize * 1.35);
const charWidth = fontSize * 0.6;
return (
<div
style={{
width: '100%',
height: '100%',
background: '#0d0d0d',
position: 'relative',
fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", "SF Mono", Monaco, monospace',
overflow: 'hidden',
}}
>
{/* Previous terminal output lines (fake history for realism) */}
<div
style={{
padding: '16px 20px',
fontSize: fontSize * 0.65,
lineHeight: `${Math.round(fontSize * 0.65 * 1.4)}px`,
color: '#495057',
}}
>
<div>
<span style={{ color: '#339af0' }}>❯</span>
<span style={{ color: '#495057' }}> claude --dangerously-skip-permissions</span>
</div>
<div style={{ color: '#3a3a3a', marginTop: 4 }}>
╭────────────────────────────────────╮
</div>
<div style={{ color: '#3a3a3a' }}>
│ Claude Code session active │
</div>
<div style={{ color: '#3a3a3a' }}>
╰────────────────────────────────────╯
</div>
</div>
{/* Active prompt line — this is where the typing happens */}
<div
style={{
padding: '8px 20px',
fontSize,
lineHeight: `${lineHeight}px`,
display: 'flex',
alignItems: 'baseline',
}}
>
{/* Prompt character */}
<span style={{ color: '#339af0', fontWeight: 700, marginRight: charWidth * 0.8 }}>❯</span>
{/* Typed text */}
{overlayChars ? (
// Zerolag mode: show overlay chars with confirmed/unconfirmed color
overlayChars.map((oc, i) => (
<span
key={i}
style={{
color: oc.confirmed ? '#e0e0e0' : '#7a7a7a',
letterSpacing: '0.5px',
}}
>
{oc.char}
</span>
))
) : (
<span style={{ color: '#e0e0e0', letterSpacing: '0.5px' }}>{typed}</span>
)}
{/* Block cursor */}
<span
style={{
display: 'inline-block',
width: charWidth,
height: lineHeight * 0.85,
background: `rgba(224, 224, 224, ${cursorOpacity})`,
verticalAlign: 'text-bottom',
marginLeft: 1,
}}
/>
</div>
</div>
);
};
+118
View File
@@ -0,0 +1,118 @@
import React from 'react';
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { colors, fonts } from '../lib/theme';
type TitleCardProps = {
text: string;
subtitle: string;
showUrl?: boolean;
};
export const TitleCard: React.FC<TitleCardProps> = ({
text,
subtitle,
showUrl = false,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Logo fade + scale in
const logoOpacity = interpolate(frame, [0, 0.5 * fps], [0, 1], {
extrapolateRight: 'clamp',
});
const logoScale = interpolate(frame, [0, 0.5 * fps], [0.8, 1], {
extrapolateRight: 'clamp',
});
// Subtitle fades in slightly after logo
const subtitleOpacity = interpolate(
frame,
[0.3 * fps, 0.8 * fps],
[0, 1],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
);
// URL fades in last
const urlOpacity = showUrl
? interpolate(frame, [0.6 * fps, 1.1 * fps], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
})
: 0;
return (
<AbsoluteFill
style={{
backgroundColor: colors.bg.dark,
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* Lightning bolt icon */}
<div
style={{
opacity: logoOpacity,
transform: `scale(${logoScale})`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 16,
}}
>
<svg width="64" height="64" viewBox="0 0 32 32">
<defs>
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#60a5fa" />
<stop offset="100%" stopColor="#3b82f6" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="6" fill="#0a0a0a" />
<path d="M18 4L8 18h6l-2 10 10-14h-6z" fill="url(#g)" />
</svg>
<div
style={{
fontFamily: fonts.ui,
fontSize: 72,
fontWeight: 700,
color: colors.accent.blue,
letterSpacing: -1,
}}
>
{text}
</div>
</div>
<div
style={{
opacity: subtitleOpacity,
fontFamily: fonts.ui,
fontSize: 28,
color: colors.text.dim,
marginTop: 12,
}}
>
{subtitle}
</div>
{showUrl && (
<div
style={{
opacity: urlOpacity,
fontFamily: fonts.mono,
fontSize: 18,
color: colors.text.muted,
marginTop: 24,
}}
>
github.com/Ark0N/Codeman
</div>
)}
</AbsoluteFill>
);
};
@@ -0,0 +1,485 @@
import React from 'react';
import {
AbsoluteFill,
Img,
interpolate,
Sequence,
spring,
staticFile,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { colors } from '../lib/theme';
import { TitleCard } from '../components/TitleCard';
import { PhoneFrame } from '../components/PhoneFrame';
import { ClickCursor } from '../components/ClickCursor';
// ─── Scene timing (frames @ 30fps) ───
const TITLE_START = 0;
const TITLE_DUR = 45;
const DESKTOP_START = 45;
const WELCOME_DUR = 75; // 2.5s — show welcome screen
const CLAUDE_DUR = 90; // 3s — single Claude tab
const BOTH_OPENCODE_DUR = 90; // 3s — both tabs, OpenCode active
const TAB_SWITCH_DUR = 120; // 4s — tab switching animation
const DESKTOP_DUR = WELCOME_DUR + CLAUDE_DUR + BOTH_OPENCODE_DUR + TAB_SWITCH_DUR; // 375
const TRANSITION_START = DESKTOP_START + DESKTOP_DUR; // 420
const TRANSITION_DUR = 60;
const MOBILE_START = TRANSITION_START + TRANSITION_DUR; // 480
const MOBILE_DUR = 240;
const OUTRO_START = MOBILE_START + MOBILE_DUR; // 720
const OUTRO_DUR = 75;
export const TOTAL_FRAMES = OUTRO_START + OUTRO_DUR; // 795
// ─── Screenshot paths ───
const SCREENSHOTS = {
desktopWelcome: staticFile('desktop-welcome.png'),
desktopClaude: staticFile('desktop-claude.png'),
desktopBothClaude: staticFile('desktop-both-claude.png'),
desktopBothOpencode: staticFile('desktop-both-opencode.png'),
mobileClaude: staticFile('mobile-claude.png'),
mobileOpencode: staticFile('mobile-opencode.png'),
};
// ─── Desktop scene ───
const DesktopDemo: React.FC = () => {
const frame = useCurrentFrame();
// Key frames within the desktop scene (local frames)
const claudeClickFrame = WELCOME_DUR - 15; // cursor clicks near end of welcome
const claudeStart = WELCOME_DUR;
const bothStart = WELCOME_DUR + CLAUDE_DUR; // both tabs appear
const switchBase = bothStart + BOTH_OPENCODE_DUR;
const switch1Frame = switchBase + 30; // switch to Claude
const switch2Frame = switchBase + 75; // switch back to OpenCode
// Determine which screenshot to show and cross-fade
// Phase 1: Welcome (0 → WELCOME_DUR)
// Phase 2: Claude single tab (WELCOME_DUR → bothStart)
// Phase 3: Both tabs, OpenCode active (bothStart → switchBase)
// Phase 4: Tab switching — OpenCode→Claude→OpenCode (switchBase → end)
// Cross-fade durations
const FADE = 10;
// Welcome → Claude cross-fade
const welcomeOpacity =
frame < claudeStart - FADE
? 1
: frame < claudeStart
? interpolate(frame, [claudeStart - FADE, claudeStart], [1, 0], {
extrapolateRight: 'clamp',
})
: 0;
const claudeSoloOpacity =
frame < claudeStart
? 0
: frame < claudeStart + FADE
? interpolate(frame, [claudeStart, claudeStart + FADE], [0, 1], {
extrapolateRight: 'clamp',
})
: frame < bothStart - FADE
? 1
: frame < bothStart
? interpolate(frame, [bothStart - FADE, bothStart], [1, 0], {
extrapolateRight: 'clamp',
})
: 0;
// Both tabs — determine which one is active
let bothOpenCodeOpacity = 0;
let bothClaudeOpacity = 0;
if (frame >= bothStart) {
// Fade in both-opencode initially
if (frame < bothStart + FADE) {
bothOpenCodeOpacity = interpolate(
frame,
[bothStart, bothStart + FADE],
[0, 1],
{ extrapolateRight: 'clamp' },
);
} else if (frame < switch1Frame) {
// Showing OpenCode
bothOpenCodeOpacity = 1;
bothClaudeOpacity = 0;
} else if (frame < switch1Frame + FADE) {
// Cross-fade OpenCode → Claude
const t = interpolate(
frame,
[switch1Frame, switch1Frame + FADE],
[0, 1],
{ extrapolateRight: 'clamp' },
);
bothOpenCodeOpacity = 1 - t;
bothClaudeOpacity = t;
} else if (frame < switch2Frame) {
// Showing Claude
bothOpenCodeOpacity = 0;
bothClaudeOpacity = 1;
} else if (frame < switch2Frame + FADE) {
// Cross-fade Claude → OpenCode
const t = interpolate(
frame,
[switch2Frame, switch2Frame + FADE],
[0, 1],
{ extrapolateRight: 'clamp' },
);
bothOpenCodeOpacity = t;
bothClaudeOpacity = 1 - t;
} else {
// Back to OpenCode
bothOpenCodeOpacity = 1;
bothClaudeOpacity = 0;
}
}
// Welcome button positions for cursor (measured from actual screenshot)
const welcomeBtnClaudeX = 651;
const welcomeBtnClaudeY = 434;
const welcomeBtnOpenCodeX = 803;
const welcomeBtnOpenCodeY = 434;
return (
<AbsoluteFill style={{ background: colors.bg.dark }}>
{/* Welcome screenshot */}
{welcomeOpacity > 0 && (
<AbsoluteFill style={{ opacity: welcomeOpacity }}>
<Img
src={SCREENSHOTS.desktopWelcome}
style={{ width: 1920, height: 1080 }}
/>
</AbsoluteFill>
)}
{/* Single Claude tab screenshot */}
{claudeSoloOpacity > 0 && (
<AbsoluteFill style={{ opacity: claudeSoloOpacity }}>
<Img
src={SCREENSHOTS.desktopClaude}
style={{ width: 1920, height: 1080 }}
/>
</AbsoluteFill>
)}
{/* Both tabs — OpenCode active */}
{bothOpenCodeOpacity > 0 && (
<AbsoluteFill style={{ opacity: bothOpenCodeOpacity }}>
<Img
src={SCREENSHOTS.desktopBothOpencode}
style={{ width: 1920, height: 1080 }}
/>
</AbsoluteFill>
)}
{/* Both tabs — Claude active */}
{bothClaudeOpacity > 0 && (
<AbsoluteFill style={{ opacity: bothClaudeOpacity }}>
<Img
src={SCREENSHOTS.desktopBothClaude}
style={{ width: 1920, height: 1080 }}
/>
</AbsoluteFill>
)}
{/* Cursor: click Claude Code button on welcome screen */}
<Sequence
from={claudeClickFrame - 20}
durationInFrames={35}
layout="none"
>
<ClickCursor
fromX={960}
fromY={300}
x={welcomeBtnClaudeX}
y={welcomeBtnClaudeY}
moveStart={0}
clickAt={20}
/>
</Sequence>
{/* Cursor: click to add OpenCode tab */}
<Sequence
from={bothStart - 20}
durationInFrames={35}
layout="none"
>
<ClickCursor
fromX={welcomeBtnClaudeX}
fromY={welcomeBtnClaudeY}
x={welcomeBtnOpenCodeX}
y={welcomeBtnOpenCodeY}
moveStart={0}
clickAt={20}
/>
</Sequence>
{/* Cursor: switch to Claude tab (first tab in header) */}
<Sequence
from={switch1Frame - 15}
durationInFrames={20}
layout="none"
>
<ClickCursor
fromX={500}
fromY={300}
x={128}
y={17}
moveStart={0}
clickAt={15}
/>
</Sequence>
{/* Cursor: switch back to OpenCode tab (second tab in header) */}
<Sequence
from={switch2Frame - 15}
durationInFrames={20}
layout="none"
>
<ClickCursor
fromX={128}
fromY={17}
x={245}
y={17}
moveStart={0}
clickAt={15}
/>
</Sequence>
</AbsoluteFill>
);
};
// ─── Device transition ───
const DeviceTransition: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Desktop shrinks down
const shrinkProgress = spring({
frame,
fps,
config: { damping: 200 },
durationInFrames: 45,
});
const desktopScale = interpolate(shrinkProgress, [0, 1], [1, 0.35]);
const desktopOpacity = interpolate(shrinkProgress, [0, 1], [1, 0], {
extrapolateRight: 'clamp',
});
// Phone frame fades in
const phoneOpacity = interpolate(frame, [20, 40], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const phoneScale = interpolate(frame, [20, 50], [0.8, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill
style={{
background: colors.bg.dark,
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* Shrinking desktop screenshot */}
<div
style={{
position: 'absolute',
width: 1920,
height: 1080,
transform: `scale(${desktopScale})`,
opacity: desktopOpacity,
}}
>
<Img
src={SCREENSHOTS.desktopBothClaude}
style={{ width: 1920, height: 1080 }}
/>
</div>
{/* Phone frame appearing */}
<div
style={{
opacity: phoneOpacity,
transform: `scale(${phoneScale})`,
}}
>
<PhoneFrame width={375} height={700}>
<Img
src={SCREENSHOTS.mobileClaude}
style={{ width: 375, height: 700, objectFit: 'cover' }}
/>
</PhoneFrame>
</div>
</AbsoluteFill>
);
};
// ─── Mobile scene ───
const MobileDemo: React.FC = () => {
const frame = useCurrentFrame();
// Mobile: Claude for 90 frames, then swipe to OpenCode
const swipeFrame = 90;
const swipeDuration = 20;
const isOpenCode = frame >= swipeFrame + swipeDuration;
const isSwiping =
frame >= swipeFrame && frame < swipeFrame + swipeDuration;
// Slide offset
const slideProgress = isSwiping
? interpolate(frame - swipeFrame, [0, swipeDuration], [0, 1], {
extrapolateRight: 'clamp',
})
: isOpenCode
? 1
: 0;
const slideX = interpolate(slideProgress, [0, 1], [0, -375]);
return (
<PhoneFrame width={375} height={700}>
<div
style={{
width: 375,
height: 700,
overflow: 'hidden',
position: 'relative',
}}
>
{/* Sliding container with both screenshots */}
<div
style={{
display: 'flex',
width: 750,
height: 700,
transform: `translateX(${slideX}px)`,
}}
>
<Img
src={SCREENSHOTS.mobileClaude}
style={{ width: 375, height: 700, flexShrink: 0, objectFit: 'cover' }}
/>
<Img
src={SCREENSHOTS.mobileOpencode}
style={{ width: 375, height: 700, flexShrink: 0, objectFit: 'cover' }}
/>
</div>
{/* Swipe indicator */}
{isSwiping && (
<SwipeIndicator
frame={frame - swipeFrame}
duration={swipeDuration}
/>
)}
</div>
</PhoneFrame>
);
};
// Swipe gesture indicator
const SwipeIndicator: React.FC<{ frame: number; duration: number }> = ({
frame,
duration,
}) => {
const progress = interpolate(frame, [0, duration], [0, 1], {
extrapolateRight: 'clamp',
});
const x = interpolate(progress, [0, 1], [300, 75]);
const opacity = interpolate(
progress,
[0, 0.2, 0.8, 1],
[0, 0.7, 0.7, 0],
{ extrapolateRight: 'clamp' },
);
return (
<div
style={{
position: 'absolute',
bottom: 60,
left: x,
width: 40,
height: 40,
borderRadius: '50%',
background: 'rgba(255,255,255,0.15)',
border: '2px solid rgba(255,255,255,0.3)',
opacity,
zIndex: 20,
}}
/>
);
};
// ─── Main composition ───
export const CodemanDemo: React.FC = () => {
return (
<AbsoluteFill style={{ background: colors.bg.dark }}>
{/* Scene 1: Title */}
<Sequence durationInFrames={TITLE_DUR} premountFor={0}>
<TitleCard text="Codeman" subtitle="AI Session Manager" />
</Sequence>
{/* Scene 2-5: Desktop flow (cross-fading screenshots) */}
<Sequence
from={DESKTOP_START}
durationInFrames={DESKTOP_DUR}
premountFor={15}
>
<DesktopDemo />
</Sequence>
{/* Scene 6: Desktop → Mobile transition */}
<Sequence
from={TRANSITION_START}
durationInFrames={TRANSITION_DUR}
premountFor={15}
>
<DeviceTransition />
</Sequence>
{/* Scene 7-9: Mobile flow (swipe between Claude/OpenCode) */}
<Sequence
from={MOBILE_START}
durationInFrames={MOBILE_DUR}
premountFor={15}
>
<AbsoluteFill
style={{
background: colors.bg.dark,
justifyContent: 'center',
alignItems: 'center',
}}
>
<MobileDemo />
</AbsoluteFill>
</Sequence>
{/* Scene 10: Outro */}
<Sequence
from={OUTRO_START}
durationInFrames={OUTRO_DUR}
premountFor={15}
>
<TitleCard
text="Codeman"
subtitle="Manage any AI coding tool"
showUrl
/>
</Sequence>
</AbsoluteFill>
);
};
@@ -0,0 +1,569 @@
import React from 'react';
import {
AbsoluteFill,
Img,
interpolate,
Sequence,
spring,
staticFile,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { colors, fonts } from '../lib/theme';
import { IPhone17ProFrame, SCREEN_W, SCREEN_H } from '../components/IPhone17ProFrame';
import { IOSKeyboard } from '../components/IOSKeyboard';
// ─── Scene timing (frames @ 30fps) ───
const TITLE_DUR = 60;
const PHONES_DUR = 30;
const TYPING_DUR = 610;
const HOLD_DUR = 50;
const OUTRO_DUR = 45;
const TITLE_START = 0;
const PHONES_START = TITLE_DUR; // 60
const TYPING_START = PHONES_START + PHONES_DUR; // 90
const HOLD_START = TYPING_START + TYPING_DUR; // 700
const OUTRO_START = HOLD_START + HOLD_DUR; // 750
export const ZEROLAG_TOTAL_FRAMES = OUTRO_START + OUTRO_DUR; // 795
// ─── iPhone 17 Pro safe area ───
const SAFE_AREA_TOP = 59; // Below Dynamic Island
const PHONE_SCALE = 1.12; // Scale up to fill more of the frame
// Claude Code header: tab bar + session info + prompt context from screenshot
const HEADER_H = 120;
// Terminal typing overlay: aligned with the ❯ prompt position in the Claude Code screenshot
const TERMINAL_TOP = 185;
const TERMINAL_LEFT = 14;
const TERMINAL_FONT = 21; // Slightly smaller to fit toolbar below
// Codeman toolbar from screenshot (bottom section showing /init, /clear, Run, etc.)
const TOOLBAR_H = 95;
// ─── Typing schedule ───
const CORRECT_TEXT =
'zerolag technology brings in a visual dom overlay to make typing instant, even if your codeman server is on the other side of the world';
const FRAME_GAP = 4; // ~133ms per keystroke (~75 WPM)
// Remote connection lag: 600ms–2.7s per char (18–80 frames @ 30fps).
// Periodic spikes simulate packet loss / retransmission bursts.
// TCP head-of-line blocking causes a single spike to freeze all subsequent chars.
const LAGGY_DELAYS = [
24, 30, 26, 32, 72, 28, 22, 34, 26, 30, 20, 28, 36, 24, 30, 22, 26, 34, 28, 20, 68, 30, 24, 32,
26, 22, 28, 34, 30, 26, 32, 24, 80, 22, 30, 26, 28, 34, 24, 30,
];
type KeyAction = { frame: number; char: string; lagDelay: number };
const buildSchedule = (): KeyAction[] => {
const actions: KeyAction[] = [];
const lag = (i: number) => LAGGY_DELAYS[i % LAGGY_DELAYS.length];
for (let i = 0; i < CORRECT_TEXT.length; i++) {
actions.push({ frame: i * FRAME_GAP, char: CORRECT_TEXT[i], lagDelay: lag(i) });
}
return actions;
};
const TYPING_SCHEDULE = buildSchedule();
/**
* Replay actions in order up to current frame, computing the visible text buffer.
* TCP-ordered: stops at first unresolved echo (head-of-line blocking).
*/
const computeVisibleText = (frame: number, withLag: boolean): string => {
let buffer = '';
for (const a of TYPING_SCHEDULE) {
const threshold = withLag ? a.frame + a.lagDelay : a.frame;
if (frame < threshold) break;
buffer += a.char;
}
return buffer;
};
// ─── iOS Status Bar (sits in the safe area, flanking Dynamic Island) ───
const IOSStatusBar: React.FC = () => (
<div
style={{
position: 'absolute',
top: 17,
left: 0,
right: 0,
height: 20,
display: 'flex',
justifyContent: 'space-between',
padding: '0 30px',
fontSize: 15,
fontFamily: fonts.ui,
fontWeight: 600,
color: '#fff',
zIndex: 40,
}}
>
<span>9:41</span>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
{/* Signal */}
<svg width="17" height="12" viewBox="0 0 17 12">
<rect x="0" y="8" width="3" height="4" rx="0.5" fill="#fff" />
<rect x="4.5" y="5" width="3" height="7" rx="0.5" fill="#fff" />
<rect x="9" y="2" width="3" height="10" rx="0.5" fill="#fff" />
<rect x="13.5" y="0" width="3" height="12" rx="0.5" fill="#fff" />
</svg>
{/* WiFi */}
<svg width="16" height="12" viewBox="0 0 16 12">
<path
d="M4.5 8.5C5.5 7.2 6.7 6.5 8 6.5s2.5.7 3.5 2"
stroke="#fff"
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
/>
<path
d="M1.5 5.5C3.5 3 5.7 1.5 8 1.5s4.5 1.5 6.5 4"
stroke="#fff"
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
/>
<circle cx="8" cy="11" r="1.5" fill="#fff" />
</svg>
{/* Battery */}
<svg width="27" height="12" viewBox="0 0 27 12">
<rect x="0" y="0.5" width="23" height="11" rx="2" stroke="#fff" strokeWidth="1" fill="none" />
<rect x="24" y="3.5" width="2.5" height="5" rx="1" fill="#fff" opacity="0.4" />
<rect x="1.5" y="2" width="20" height="8" rx="1" fill="#32d74b" />
</svg>
</div>
</div>
);
// ─── Typing overlay ───
const TypingOverlay: React.FC<{
typed: string;
cursorVisible: boolean;
}> = ({ typed, cursorVisible }) => {
const frame = useCurrentFrame();
const cursorOpacity = cursorVisible ? (Math.floor(frame / 18) % 2 === 0 ? 0.85 : 0.5) : 0;
const lineH = Math.round(TERMINAL_FONT * 1.4);
const charW = TERMINAL_FONT * 0.62;
return (
<div
style={{
position: 'absolute',
top: TERMINAL_TOP,
left: TERMINAL_LEFT,
right: TERMINAL_LEFT,
fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", "SF Mono", Monaco, monospace',
fontSize: TERMINAL_FONT,
lineHeight: `${lineH}px`,
zIndex: 10,
}}
>
<span style={{ color: '#339af0', fontWeight: 700 }}>{'❯ '}</span>
<span style={{ color: '#e0e0e0' }}>{typed}</span>
<span
style={{
display: 'inline-block',
width: charW,
height: lineH * 0.82,
background: `rgba(224, 224, 224, ${cursorOpacity})`,
verticalAlign: 'text-bottom',
marginLeft: 1,
}}
/>
</div>
);
};
// ─── Single phone: iPhone 17 Pro + Claude Code header + typing + keyboard ───
const MobileCodeman: React.FC<{
typed: string;
cursorVisible: boolean;
activeKey?: string;
pressAge?: number;
showKeyboard?: boolean;
noAnimation?: boolean;
showPromo?: boolean;
}> = ({ typed, cursorVisible, activeKey, pressAge, showKeyboard = true, noAnimation, showPromo }) => (
<IPhone17ProFrame noAnimation={noAnimation}>
<div
style={{ width: SCREEN_W, height: SCREEN_H, position: 'relative', overflow: 'hidden', background: '#0d0d0d' }}
>
{/* iOS status bar in the safe area */}
<IOSStatusBar />
{/* Claude Code header from screenshot (tabs + session info) */}
<div
style={{
position: 'absolute',
top: SAFE_AREA_TOP,
left: 0,
right: 0,
height: HEADER_H,
overflow: 'hidden',
zIndex: 5,
}}
>
<Img
src={staticFile('mobile-claude.png')}
style={{
width: SCREEN_W,
objectFit: 'cover',
objectPosition: 'top left',
}}
/>
{/* Fade to terminal background */}
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 30,
background: 'linear-gradient(transparent, #0d0d0d)',
}}
/>
</div>
{/* Dark mask: hides screenshot text below Claude Code info (e.g. "Try edit...") */}
<div
style={{
position: 'absolute',
top: SAFE_AREA_TOP + 95,
left: 0,
right: 0,
bottom: 0,
zIndex: 8,
}}
>
{/* Smooth gradient blend from screenshot into dark terminal */}
<div
style={{
height: 18,
background: 'linear-gradient(transparent, #0d0d0d)',
}}
/>
<div style={{ flex: 1, background: '#0d0d0d' }} />
</div>
{/* Typing animation */}
<TypingOverlay typed={typed} cursorVisible={cursorVisible} />
{/* Codeman toolbar from screenshot (/init, /clear, /compact, Run, Run Shell, voice) */}
<div
style={{
position: 'absolute',
bottom: 232,
left: 0,
right: 0,
height: TOOLBAR_H,
overflow: 'hidden',
zIndex: 15,
}}
>
{/* Gradient blend at top */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 18,
background: 'linear-gradient(#0d0d0d, transparent)',
zIndex: 1,
}}
/>
<Img
src={staticFile('mobile-claude.png')}
style={{
width: SCREEN_W,
position: 'absolute',
bottom: 0,
}}
/>
</div>
{/* Promo cards between text and toolbar (left phone only) */}
{showPromo && <PromoBanners />}
{/* iOS keyboard at bottom */}
{showKeyboard && (
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 20 }}>
<IOSKeyboard activeKey={activeKey} pressAge={pressAge} />
</div>
)}
</div>
</IPhone17ProFrame>
);
// ─── Label above phone (prominent header) ───
const PhoneLabel: React.FC<{
title: string;
detail: string;
dotColor: string;
detailColor: string;
}> = ({ title, detail, dotColor, detailColor }) => (
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
fontSize: 28,
fontWeight: 700,
fontFamily: fonts.ui,
color: '#fff',
}}
>
<div
style={{
width: 12,
height: 12,
borderRadius: '50%',
background: dotColor,
boxShadow: `0 0 12px ${dotColor}`,
}}
/>
{title}
</div>
<div style={{ fontSize: 16, fontFamily: fonts.mono, color: detailColor, marginTop: 6, opacity: 0.9 }}>
{detail}
</div>
</div>
);
// ─── Promo banners (inside left phone, between text and keyboard) ───
const PromoBanners: React.FC = () => (
<div
style={{
position: 'absolute',
bottom: 332,
left: 8,
right: 8,
display: 'flex',
flexDirection: 'column',
gap: 8,
zIndex: 15,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 14px',
background: 'rgba(255,255,255,0.06)',
borderRadius: 12,
border: '1px solid rgba(255,255,255,0.1)',
}}
>
<svg width="30" height="30" viewBox="0 0 16 16" fill="#ccc" style={{ flexShrink: 0 }}>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<div>
<div style={{ fontSize: 18, fontWeight: 700, fontFamily: fonts.ui, color: '#e0e0e0' }}>Codeman</div>
<div style={{ fontSize: 12, fontFamily: fonts.mono, color: '#888' }}>github.com/Ark0N/Codeman</div>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 14px',
background: 'rgba(255,255,255,0.06)',
borderRadius: 12,
border: '1px solid rgba(255,255,255,0.1)',
}}
>
<svg width="30" height="30" viewBox="0 0 16 16" style={{ flexShrink: 0 }}>
<rect width="16" height="16" rx="2" fill="#cb3837" />
<path d="M3 3h10v10H8V5H5v8H3z" fill="#fff" />
</svg>
<div>
<div style={{ fontSize: 18, fontWeight: 700, fontFamily: fonts.ui, color: '#e0e0e0' }}>xterm-zerolag-input</div>
<div style={{ fontSize: 12, fontFamily: fonts.mono, color: '#888' }}>npmjs.com/package/xterm-zerolag-input</div>
</div>
</div>
</div>
);
// ─── Title scene ───
const TitleScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const titleScale = spring({ frame, fps, config: { damping: 15, stiffness: 80 } });
const titleOpacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
const subtitleOpacity = interpolate(frame, [15, 35], [0, 1], { extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ background: colors.bg.dark, justifyContent: 'center', alignItems: 'center' }}>
<div style={{ textAlign: 'center', transform: `scale(${titleScale})` }}>
<div
style={{
fontSize: 80,
fontWeight: 700,
fontFamily: fonts.ui,
color: '#fff',
opacity: titleOpacity,
letterSpacing: -1.5,
}}
>
Zerolag Input
</div>
<div
style={{
fontSize: 30,
fontFamily: fonts.ui,
color: colors.text.dim,
opacity: subtitleOpacity,
marginTop: 16,
}}
>
Local echo for remote terminal sessions
</div>
</div>
</AbsoluteFill>
);
};
// ─── Outro ───
const OutroScene: React.FC = () => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ background: colors.bg.dark, justifyContent: 'center', alignItems: 'center', opacity }}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 56, fontWeight: 700, fontFamily: fonts.ui, color: '#fff' }}>Codeman</div>
<div style={{ fontSize: 26, fontFamily: fonts.ui, color: colors.accent.green, marginTop: 10 }}>
Zero-latency mobile input
</div>
<div style={{ fontSize: 16, fontFamily: fonts.mono, color: colors.text.muted, marginTop: 20 }}>
npm i xterm-zerolag-input
</div>
</div>
</AbsoluteFill>
);
};
// ─── Typing demo scene ───
const TypingDemo: React.FC = () => {
const frame = useCurrentFrame();
const laggyTyped = computeVisibleText(frame, true);
const zerolagTyped = computeVisibleText(frame, false);
let activeKey: string | undefined;
let pressAge = 99;
for (let i = TYPING_SCHEDULE.length - 1; i >= 0; i--) {
const ev = TYPING_SCHEDULE[i];
if (frame >= ev.frame && frame < ev.frame + 5) {
activeKey = ev.char;
pressAge = frame - ev.frame;
break;
}
}
return (
<AbsoluteFill style={{ background: colors.bg.dark, justifyContent: 'center', alignItems: 'center' }}>
<div
style={{
display: 'flex',
gap: 50,
alignItems: 'flex-start',
transform: `scale(${PHONE_SCALE})`,
transformOrigin: 'center center',
}}
>
<div>
<PhoneLabel
title="With Zerolag"
detail="0ms local echo"
dotColor={colors.accent.green}
detailColor={colors.accent.green}
/>
<MobileCodeman typed={zerolagTyped} cursorVisible activeKey={activeKey} pressAge={pressAge} noAnimation showPromo />
</div>
<div>
<PhoneLabel
title="Without Zerolag"
detail="600ms–2.7s server echo"
dotColor={colors.accent.red}
detailColor={colors.accent.red}
/>
<MobileCodeman typed={laggyTyped} cursorVisible activeKey={activeKey} pressAge={pressAge} noAnimation />
</div>
</div>
</AbsoluteFill>
);
};
// ─── Phones entrance ───
const PanelsEntrance: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps, config: { damping: 15, stiffness: 80 } });
const opacity = interpolate(frame, [0, 10], [0, 1], { extrapolateRight: 'clamp' });
return (
<AbsoluteFill
style={{
background: colors.bg.dark,
justifyContent: 'center',
alignItems: 'center',
opacity,
transform: `scale(${scale * PHONE_SCALE})`,
}}
>
<div style={{ display: 'flex', gap: 50, alignItems: 'flex-start' }}>
<div>
<PhoneLabel
title="With Zerolag"
detail="0ms local echo"
dotColor={colors.accent.green}
detailColor={colors.accent.green}
/>
<MobileCodeman typed="" cursorVisible noAnimation />
</div>
<div>
<PhoneLabel
title="Without Zerolag"
detail="600ms–2.7s server echo"
dotColor={colors.accent.red}
detailColor={colors.accent.red}
/>
<MobileCodeman typed="" cursorVisible noAnimation />
</div>
</div>
</AbsoluteFill>
);
};
// ─── Main composition ───
export const ZerolagDemo: React.FC = () => {
return (
<AbsoluteFill style={{ background: colors.bg.dark }}>
<Sequence from={TITLE_START} durationInFrames={TITLE_DUR}>
<TitleScene />
</Sequence>
<Sequence from={PHONES_START} durationInFrames={PHONES_DUR} premountFor={5}>
<PanelsEntrance />
</Sequence>
<Sequence from={TYPING_START} durationInFrames={TYPING_DUR + HOLD_DUR} premountFor={5}>
<TypingDemo />
</Sequence>
<Sequence from={OUTRO_START} durationInFrames={OUTRO_DUR} premountFor={5}>
<OutroScene />
</Sequence>
</AbsoluteFill>
);
};
+4
View File
@@ -0,0 +1,4 @@
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);
+42
View File
@@ -0,0 +1,42 @@
// Design tokens extracted from Codeman's styles.css and index.html
export const colors = {
bg: {
dark: '#0a0a0a',
card: '#141414',
input: '#1a1a1a',
hover: '#222',
terminal: '#0d0d0d',
},
border: {
default: '#2a2a2a',
light: '#333',
},
text: {
primary: '#eee',
dim: '#888',
muted: '#555',
},
accent: {
blue: '#3b82f6',
blueHover: '#60a5fa',
green: '#22c55e',
yellow: '#eab308',
red: '#ef4444',
purple: '#a855f7',
orange: '#f97316',
},
} as const;
export const layout = {
headerHeight: 36,
toolbarHeight: 40,
tabHeight: 28,
borderRadius: 4,
} as const;
export const fonts = {
ui: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
mono: "'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace",
} as const;
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB