chore: version packages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-04 12:17:38 +01:00
co-authored by Claude Opus 4.6
parent 49e9b9e8c9
commit 9433de75b8
12 changed files with 1291 additions and 12 deletions
+6
View File
@@ -1,5 +1,11 @@
# aicodeman
## 0.3.2
### Patch Changes
- Make file browser panel draggable by its header
## 0.3.1
### Patch Changes
+1 -1
View File
@@ -52,7 +52,7 @@ When user says "COM":
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
**Version**: 0.3.1 (must match `package.json`)
**Version**: 0.3.2 (must match `package.json`)
## Project Overview
+316 -2
View File
@@ -1,12 +1,12 @@
{
"name": "aicodeman",
"version": "0.3.0",
"version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aicodeman",
"version": "0.3.0",
"version": "0.3.1",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -17,6 +17,8 @@
"@fastify/compress": "^8.3.1",
"@fastify/cookie": "^11.0.2",
"@fastify/static": "^8.0.0",
"@remotion/compositor-linux-x64-gnu": "^4.0.432",
"@rspack/binding-linux-x64-gnu": "^1.7.7",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"commander": "^12.1.0",
@@ -515,6 +517,40 @@
"node": ">=18"
}
},
"node_modules/@emnapi/core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
"integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"dev": true,
@@ -1163,6 +1199,19 @@
"@module-federation/sdk": "0.22.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"dev": true,
@@ -1324,6 +1373,95 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/@remotion/compositor-darwin-arm64": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.429.tgz",
"integrity": "sha512-V3Cv7TZyKulBPOBQJu2GONiKA+DDjGT6rWAc3WX4M9J9EDUwKiFFTDuTEpC7s4mcUDoAUXbnYE8ZlnWGaAuJhQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@remotion/compositor-darwin-x64": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.429.tgz",
"integrity": "sha512-vB8NM0j7gvCniu1dZ0p4MmTDN44Dvgd2iq6vStigzEQ1P2vlcx7qHEdVfaZBHsjdjMXD/UdwNmb34+lw3v6KEw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@remotion/compositor-linux-arm64-gnu": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.429.tgz",
"integrity": "sha512-F8N1HsRiCDw1EWWWI9hLNFTQy1mY7QLsWh0ywnrNCx62aF+8NpdYdW4pt/e7s4SQmkLh7enrkuqI0a6aGw4rpw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@remotion/compositor-linux-arm64-musl": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.429.tgz",
"integrity": "sha512-rvDjjascY9HAQ1nfbrdlVxyQ/xYj84jf0LylLmWJgr0kZVw5l8D/QXH4HTQeoPKhM3cBsCMKpW7oseHMoDyM5A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@remotion/compositor-linux-x64-gnu": {
"version": "4.0.432",
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.432.tgz",
"integrity": "sha512-KR/oSdCX6PBjO6K2KhYpC+QBbVMo8aXwQoTHaVwNl94+t7tyXDj7Vi8b3EJaIDSeXAc7lNjs54Tzx/WQ615Avg==",
"cpu": [
"x64"
],
"os": [
"linux"
]
},
"node_modules/@remotion/compositor-linux-x64-musl": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.429.tgz",
"integrity": "sha512-tlgWyrOWFDw1Z8IyPGDrQhd2B3saZ/sGRC7nN0XhcbMnaq3wxWe+cUSCJPT9kFNR9tKNCZ0y/jG/B8h6Z0GiHQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@remotion/compositor-win32-x64-msvc": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.429.tgz",
"integrity": "sha512-YnpRTVwrOnDA8nYCZEP6Tdx+Ea+LQO4kXm9sjg2Rxe3s55lt+dUURl1yvOSKTW8nKZlfDAqldI1YDVvfD8NnCQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@remotion/licensing": {
"version": "4.0.429",
"dev": true,
@@ -1393,6 +1531,19 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/@remotion/renderer/node_modules/@remotion/compositor-linux-x64-gnu": {
"version": "4.0.429",
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.429.tgz",
"integrity": "sha512-YRe6+oSnNFpc2jgkrmAISAO2xUA0LqzE49ykqe0zCH3MXKtof76lMwDPoFJT1C4vDKErjSedltQNnnG7bPQGlw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@remotion/renderer/node_modules/source-map": {
"version": "0.8.0-beta.0",
"dev": true,
@@ -1646,6 +1797,158 @@
"@rspack/binding-win32-x64-msvc": "1.7.6"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.6.tgz",
"integrity": "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.6.tgz",
"integrity": "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.6.tgz",
"integrity": "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.6.tgz",
"integrity": "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.7.tgz",
"integrity": "sha512-aRvf8gCI7jDeEN9i4u9fY5coa3ZAyHzGVA4ZhTJCgZ5wWA5A9SQewMSq7khS1WAAFE1USlk1tUuPujnrGoYrGg==",
"cpu": [
"x64"
],
"license": "MIT",
"os": [
"linux"
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.6.tgz",
"integrity": "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rspack/binding-wasm32-wasi": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.6.tgz",
"integrity": "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "1.0.7"
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.6.tgz",
"integrity": "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.6.tgz",
"integrity": "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.6.tgz",
"integrity": "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rspack/binding/node_modules/@rspack/binding-linux-x64-gnu": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.6.tgz",
"integrity": "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rspack/core": {
"version": "1.7.6",
"dev": true,
@@ -1700,6 +2003,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"dev": true,
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aicodeman",
"version": "0.3.1",
"version": "0.3.2",
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
@@ -51,6 +51,8 @@
"@fastify/compress": "^8.3.1",
"@fastify/cookie": "^11.0.2",
"@fastify/static": "^8.0.0",
"@remotion/compositor-linux-x64-gnu": "^4.0.432",
"@rspack/binding-linux-x64-gnu": "^1.7.7",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"commander": "^12.1.0",
Symlink
+1
View File
@@ -0,0 +1 @@
tools/remotion/public
+61
View File
@@ -3560,6 +3560,24 @@ class CodemanApp {
if (fileBrowserPanel) {
fileBrowserPanel.classList.add('visible');
this.loadFileBrowser(sessionId);
// Attach drag listeners if not already attached
if (!this.fileBrowserDragListeners) {
const header = fileBrowserPanel.querySelector('.file-browser-header');
if (header) {
const onFirstDrag = () => {
if (!fileBrowserPanel.style.left) {
const rect = fileBrowserPanel.getBoundingClientRect();
fileBrowserPanel.style.left = `${rect.left}px`;
fileBrowserPanel.style.top = `${rect.top}px`;
fileBrowserPanel.style.right = 'auto';
}
};
header.addEventListener('mousedown', onFirstDrag);
header.addEventListener('touchstart', onFirstDrag, { passive: true });
this.fileBrowserDragListeners = this.makeWindowDraggable(fileBrowserPanel, header);
this.fileBrowserDragListeners._onFirstDrag = onFirstDrag;
}
}
}
}
});
@@ -7050,6 +7068,25 @@ class CodemanApp {
if (showFileBrowser && this.activeSessionId) {
fileBrowserPanel.classList.add('visible');
this.loadFileBrowser(this.activeSessionId);
// Attach drag listeners if not already attached
if (!this.fileBrowserDragListeners) {
const header = fileBrowserPanel.querySelector('.file-browser-header');
if (header) {
// Convert right-positioned to left/top before drag so makeWindowDraggable works
const onFirstDrag = () => {
if (!fileBrowserPanel.style.left) {
const rect = fileBrowserPanel.getBoundingClientRect();
fileBrowserPanel.style.left = `${rect.left}px`;
fileBrowserPanel.style.top = `${rect.top}px`;
fileBrowserPanel.style.right = 'auto';
}
};
header.addEventListener('mousedown', onFirstDrag);
header.addEventListener('touchstart', onFirstDrag, { passive: true });
this.fileBrowserDragListeners = this.makeWindowDraggable(fileBrowserPanel, header);
this.fileBrowserDragListeners._onFirstDrag = onFirstDrag;
}
}
} else {
fileBrowserPanel.classList.remove('visible');
}
@@ -10349,6 +10386,7 @@ class CodemanApp {
fileBrowserExpandedDirs = new Set();
fileBrowserFilter = '';
fileBrowserAllExpanded = false;
fileBrowserDragListeners = null;
filePreviewContent = '';
async loadFileBrowser(sessionId) {
@@ -10532,6 +10570,29 @@ class CodemanApp {
const panel = this.$('fileBrowserPanel');
if (panel) {
panel.classList.remove('visible');
// Reset position so it reopens at default location
panel.style.left = '';
panel.style.top = '';
panel.style.bottom = '';
panel.style.right = '';
}
// Clean up drag listeners
if (this.fileBrowserDragListeners) {
const dl = this.fileBrowserDragListeners;
document.removeEventListener('mousemove', dl.move);
document.removeEventListener('mouseup', dl.up);
document.removeEventListener('touchmove', dl.touchMove);
document.removeEventListener('touchend', dl.up);
document.removeEventListener('touchcancel', dl.up);
if (dl.handle) {
dl.handle.removeEventListener('mousedown', dl.handleMouseDown);
dl.handle.removeEventListener('touchstart', dl.handleTouchStart);
if (dl._onFirstDrag) {
dl.handle.removeEventListener('mousedown', dl._onFirstDrag);
dl.handle.removeEventListener('touchstart', dl._onFirstDrag);
}
}
this.fileBrowserDragListeners = null;
}
// Save setting
const settings = this.loadAppSettingsFromStorage();
+2
View File
@@ -5825,6 +5825,8 @@ kbd {
background: var(--bg-input);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
cursor: move;
user-select: none;
}
.file-browser-title {
+19 -8
View File
@@ -1,16 +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="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}
/>
</>
);
};
+203
View File
@@ -0,0 +1,203 @@
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);
// 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: SPECIAL_BG,
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,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>
);
};
+391
View File
@@ -0,0 +1,391 @@
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 = 420;
const HOLD_DUR = 60;
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; // 510
const OUTRO_START = HOLD_START + HOLD_DUR; // 570
export const ZEROLAG_TOTAL_FRAMES = OUTRO_START + OUTRO_DUR; // 615
// ─── 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
// The Codeman screenshot starts content at y=0 (the session tab).
// On a real device it would sit below the safe area, so we offset it.
const SCREENSHOT_Y_OFFSET = SAFE_AREA_TOP;
// Terminal typing overlay position (relative to screenshot top)
// Session tab is ~44px, then terminal starts. Adding safe area offset:
const TERMINAL_TOP = SAFE_AREA_TOP + 52;
const TERMINAL_LEFT = 14;
const TERMINAL_FONT = 22; // Large for video readability
// ─── Typing schedule ───
const TYPED_TEXT = 'fix the auth bug in the login flow';
const FRAME_GAP = 12; // ~400ms between keystrokes
// Remote connection lag: 600ms-1.2s+ per char (18-36+ frames)
const LAGGY_DELAYS = [
24, 30, 36, 32, 26, 22, 34, 28, 38, 20, 30, 24, 32, 26, 36, 22,
30, 24, 32, 28, 34, 26, 30, 22, 28, 36, 24, 30, 32, 26, 34, 28, 24, 30,
];
type KeyEvent = { frame: number; char: string; lagDelay: number };
const TYPING_SCHEDULE: KeyEvent[] = TYPED_TEXT.split('').map((char, i) => ({
frame: i * FRAME_GAP,
char,
lagDelay: LAGGY_DELAYS[i % LAGGY_DELAYS.length],
}));
const CONFIRM_DELAY = 14;
// ─── 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;
overlayChars?: { char: string; confirmed: boolean }[];
cursorVisible: boolean;
}> = ({ typed, overlayChars, 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>
{overlayChars
? overlayChars.map((oc, i) => (
<span key={i} style={{ color: oc.confirmed ? '#e0e0e0' : '#666' }}>
{oc.char}
</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 + real screenshot + typing + keyboard ───
const MobileCodeman: React.FC<{
typed: string;
overlayChars?: { char: string; confirmed: boolean }[];
cursorVisible: boolean;
activeKey?: string;
pressAge?: number;
showKeyboard?: boolean;
noAnimation?: boolean;
}> = ({ typed, overlayChars, cursorVisible, activeKey, pressAge, showKeyboard = true, noAnimation }) => (
<IPhone17ProFrame noAnimation={noAnimation}>
<div style={{ width: SCREEN_W, height: SCREEN_H, position: 'relative', overflow: 'hidden', background: '#000' }}>
{/* Real Codeman mobile screenshot, pushed down by safe area */}
<Img
src={staticFile('mobile-claude.png')}
style={{
width: SCREEN_W,
height: SCREEN_H - SCREENSHOT_Y_OFFSET,
objectFit: 'cover',
objectPosition: 'top',
position: 'absolute',
top: SCREENSHOT_Y_OFFSET,
left: 0,
}}
/>
{/* iOS status bar in the safe area */}
<IOSStatusBar />
{/* Typing animation */}
<TypingOverlay typed={typed} overlayChars={overlayChars} cursorVisible={cursorVisible} />
{/* 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 beneath phone ───
const PhoneLabel: React.FC<{
title: string;
detail: string;
dotColor: string;
detailColor: string;
}> = ({ title, detail, dotColor, detailColor }) => (
<div style={{ textAlign: 'center', marginTop: 20 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
fontSize: 24,
fontWeight: 600,
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>
);
// ─── 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 laggyChars = TYPING_SCHEDULE.filter((ev) => frame >= ev.frame + ev.lagDelay).map((ev) => ev.char);
const laggyTyped = laggyChars.join('');
const zerolagOverlay = TYPING_SCHEDULE.filter((ev) => frame >= ev.frame).map((ev) => ({
char: ev.char,
confirmed: frame >= ev.frame + CONFIRM_DELAY,
}));
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>
<MobileCodeman typed={laggyTyped} cursorVisible activeKey={activeKey} pressAge={pressAge} noAnimation />
<PhoneLabel title="Without Zerolag" detail="600ms–1.2s delay" dotColor={colors.accent.red} detailColor={colors.accent.red} />
</div>
<div>
<MobileCodeman typed="" overlayChars={zerolagOverlay} cursorVisible activeKey={activeKey} pressAge={pressAge} noAnimation />
<PhoneLabel title="With Zerolag" detail="0ms delay" dotColor={colors.accent.green} detailColor={colors.accent.green} />
</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>
<MobileCodeman typed="" cursorVisible noAnimation />
<PhoneLabel title="Without Zerolag" detail="600ms–1.2s delay" dotColor={colors.accent.red} detailColor={colors.accent.red} />
</div>
<div>
<MobileCodeman typed="" cursorVisible noAnimation />
<PhoneLabel title="With Zerolag" detail="0ms delay" dotColor={colors.accent.green} detailColor={colors.accent.green} />
</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>
);
};