mirror of
https://github.com/digitalsamba/claude-code-video-toolkit.git
synced 2026-09-18 19:41:13 +08:00
0d96c3b764
Major changes: - Shared lib/ with reusable components (AnimatedBackground, SlideTransition, Label, Vignette, LogoWatermark, SplitScreen, NarratorPiP) - Theme system (ThemeProvider, useTheme) in lib/theme/ - Multi-session project system with project.json schema - Brand loader utility and brand.ts generation Commands: - /video - unified project management (replaced /new-video, /new-sprint-video) - /brand - brand profiles (replaced /new-brand) - /template - list templates - /skills - list/create skills (renamed from /skill to avoid Claude Code conflict) - /contribute - guided contribution workflow Infrastructure: - examples/ directory for shareable showcase projects - CONTRIBUTORS.md for recognition - FEEDBACK.md for capturing improvements - Evolution narrative in all commands Fix: - Renamed /skill to /skills - naming collision with built-in Skill tool was silently preventing ALL custom commands from loading 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
199 lines
4.8 KiB
TypeScript
199 lines
4.8 KiB
TypeScript
/**
|
|
* SplitScreen - Side-by-side video comparison layout
|
|
*
|
|
* Displays two videos side by side with labels. Useful for showing
|
|
* mobile vs desktop, before/after, or parallel features.
|
|
*/
|
|
|
|
import { AbsoluteFill, OffthreadVideo, staticFile, getStaticFiles } from 'remotion';
|
|
import { useTheme } from '../theme';
|
|
import { Label } from './Label';
|
|
|
|
export interface SplitScreenProps {
|
|
leftVideo: string;
|
|
rightVideo: string;
|
|
leftLabel?: string;
|
|
rightLabel?: string;
|
|
bottomLabel?: string;
|
|
jiraRef?: string;
|
|
/** Shared start offset for both videos (frames) */
|
|
startFrom?: number;
|
|
/** Individual start offset for left video (frames) */
|
|
leftStartFrom?: number;
|
|
/** Individual start offset for right video (frames) */
|
|
rightStartFrom?: number;
|
|
/** Playback speed multiplier */
|
|
playbackRate?: number;
|
|
/** Path prefix for video files. Default: 'demos/' */
|
|
videoPath?: string;
|
|
}
|
|
|
|
interface VideoOrPlaceholderProps {
|
|
videoFile: string;
|
|
videoPath: string;
|
|
startFrom: number;
|
|
playbackRate: number;
|
|
}
|
|
|
|
const VideoOrPlaceholder: React.FC<VideoOrPlaceholderProps> = ({
|
|
videoFile,
|
|
videoPath,
|
|
startFrom,
|
|
playbackRate,
|
|
}) => {
|
|
const theme = useTheme();
|
|
const staticFiles = getStaticFiles();
|
|
const fullPath = `${videoPath}${videoFile}`;
|
|
const hasVideo = staticFiles.some((f) => f.name === fullPath);
|
|
|
|
if (hasVideo) {
|
|
return (
|
|
<OffthreadVideo
|
|
src={staticFile(fullPath)}
|
|
startFrom={startFrom}
|
|
playbackRate={playbackRate}
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'contain',
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100%',
|
|
fontFamily: theme.fonts.primary,
|
|
}}
|
|
>
|
|
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }}>🎬</div>
|
|
<div style={{ color: theme.colors.textLight, fontSize: 16 }}>Video placeholder</div>
|
|
<div
|
|
style={{
|
|
color: theme.colors.primary,
|
|
fontSize: 14,
|
|
marginTop: 8,
|
|
fontFamily: theme.fonts.mono,
|
|
}}
|
|
>
|
|
{videoFile}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const SplitScreen: React.FC<SplitScreenProps> = ({
|
|
leftVideo,
|
|
rightVideo,
|
|
leftLabel = 'Left',
|
|
rightLabel = 'Right',
|
|
bottomLabel,
|
|
jiraRef,
|
|
startFrom = 0,
|
|
leftStartFrom,
|
|
rightStartFrom,
|
|
playbackRate = 1,
|
|
videoPath = 'demos/',
|
|
}) => {
|
|
const theme = useTheme();
|
|
|
|
// Allow individual overrides, fall back to shared startFrom
|
|
const leftOffset = leftStartFrom ?? startFrom;
|
|
const rightOffset = rightStartFrom ?? startFrom;
|
|
|
|
return (
|
|
<AbsoluteFill
|
|
style={{
|
|
backgroundColor: 'transparent',
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
}}
|
|
>
|
|
{/* Left panel */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<VideoOrPlaceholder
|
|
videoFile={leftVideo}
|
|
videoPath={videoPath}
|
|
startFrom={leftOffset}
|
|
playbackRate={playbackRate}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: 24,
|
|
left: 24,
|
|
backgroundColor: theme.colors.bgOverlay,
|
|
color: theme.colors.textDark,
|
|
padding: '14px 24px',
|
|
borderRadius: theme.borderRadius.md,
|
|
fontFamily: theme.fonts.primary,
|
|
fontSize: 32,
|
|
fontWeight: 500,
|
|
boxShadow: `0 4px 12px ${theme.colors.shadow}`,
|
|
}}
|
|
>
|
|
{leftLabel}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div
|
|
style={{
|
|
width: 4,
|
|
backgroundColor: theme.colors.divider,
|
|
}}
|
|
/>
|
|
|
|
{/* Right panel */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<VideoOrPlaceholder
|
|
videoFile={rightVideo}
|
|
videoPath={videoPath}
|
|
startFrom={rightOffset}
|
|
playbackRate={playbackRate}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: 24,
|
|
left: 24,
|
|
backgroundColor: theme.colors.bgOverlay,
|
|
color: theme.colors.textDark,
|
|
padding: '14px 24px',
|
|
borderRadius: theme.borderRadius.md,
|
|
fontFamily: theme.fonts.primary,
|
|
fontSize: 32,
|
|
fontWeight: 500,
|
|
boxShadow: `0 4px 12px ${theme.colors.shadow}`,
|
|
}}
|
|
>
|
|
{rightLabel}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom label */}
|
|
{bottomLabel && <Label text={bottomLabel} jiraRef={jiraRef} position="bottom-left" />}
|
|
</AbsoluteFill>
|
|
);
|
|
};
|