mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user