mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-07-29 05:53:50 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "video",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "remotion studio src/index.ts",
|
||||
"render": "sh -c 'mkdir -p ../dist/video && remotion render src/index.ts TrackeepTrailer16x9 ../dist/video/trackeep-trailer.mp4'",
|
||||
"render:vertical": "sh -c 'mkdir -p ../dist/video && remotion render src/index.ts TrackeepTrailer9x16 ../dist/video/trackeep-trailer-vertical.mp4'",
|
||||
"render:poster": "sh -c 'mkdir -p ../dist/video && remotion still src/index.ts TrackeepPoster16x9 ../dist/video/trackeep-poster.png'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@remotion/cli": "4.0.434",
|
||||
"@tabler/icons-react": "^3.39.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"remotion": "4.0.434"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Replace `trailer-bed.mp3` with your final music bed to keep the timing unchanged.
|
||||
The current file is a generated placeholder pulse track aligned to the 120 BPM edit grid.
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,14 @@
|
||||
import {Backdrop} from './components/Backdrop';
|
||||
import {EndCard} from './components/EndCard';
|
||||
import {endCard} from './data/trailerData';
|
||||
import {TrailerConfigProvider} from './lib/trailer-config';
|
||||
|
||||
export const Poster = () => {
|
||||
return (
|
||||
<TrailerConfigProvider layout="landscape" effects="full">
|
||||
<Backdrop>
|
||||
<EndCard frame={30} title={endCard.title} subtitle={endCard.subtitle} />
|
||||
</Backdrop>
|
||||
</TrailerConfigProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import {Composition} from 'remotion';
|
||||
import {HEIGHT, FPS, TRAILER_DURATION, WIDTH} from './data/trailerData';
|
||||
import {Poster} from './Poster';
|
||||
import {Trailer} from './Trailer';
|
||||
import {VerticalTrailer} from './VerticalTrailer';
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
<>
|
||||
<Composition
|
||||
id="TrackeepTrailer16x9"
|
||||
component={Trailer}
|
||||
durationInFrames={TRAILER_DURATION}
|
||||
fps={FPS}
|
||||
width={WIDTH}
|
||||
height={HEIGHT}
|
||||
/>
|
||||
<Composition
|
||||
id="TrackeepTrailer16x9Preview"
|
||||
component={Trailer}
|
||||
defaultProps={{effects: 'lite', includeAudio: false}}
|
||||
durationInFrames={TRAILER_DURATION}
|
||||
fps={FPS}
|
||||
width={WIDTH}
|
||||
height={HEIGHT}
|
||||
/>
|
||||
<Composition
|
||||
id="TrackeepTrailer9x16"
|
||||
component={VerticalTrailer}
|
||||
durationInFrames={TRAILER_DURATION}
|
||||
fps={FPS}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
<Composition
|
||||
id="TrackeepPoster16x9"
|
||||
component={Poster}
|
||||
durationInFrames={1}
|
||||
fps={FPS}
|
||||
width={WIDTH}
|
||||
height={HEIGHT}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import type {ComponentType} from 'react';
|
||||
import {AbsoluteFill, Sequence} from 'remotion';
|
||||
import {sceneTimings} from './data/trailerData';
|
||||
import type {SceneTiming} from './data/types';
|
||||
import {TrailerConfigProvider, type TrailerEffects} from './lib/trailer-config';
|
||||
import {TrailerAudio} from './TrailerAudio';
|
||||
import {AnalyticsScene} from './scenes/AnalyticsScene';
|
||||
import {ColdOpenScene} from './scenes/ColdOpenScene';
|
||||
import {DashboardFlybyScene} from './scenes/DashboardFlybyScene';
|
||||
import {EndCardScene} from './scenes/EndCardScene';
|
||||
import {MetricsActivationScene} from './scenes/MetricsActivationScene';
|
||||
import {MontageScene} from './scenes/MontageScene';
|
||||
import {SearchAiScene} from './scenes/SearchAiScene';
|
||||
import {TimeTrackingScene} from './scenes/TimeTrackingScene';
|
||||
|
||||
interface TrailerProps {
|
||||
effects?: TrailerEffects;
|
||||
includeAudio?: boolean;
|
||||
}
|
||||
|
||||
export const Trailer = ({effects = 'full', includeAudio = true}: TrailerProps) => {
|
||||
const sceneMap: Record<SceneTiming['id'], ComponentType> = {
|
||||
'cold-open': ColdOpenScene,
|
||||
'dashboard-flyby': DashboardFlybyScene,
|
||||
'metric-activation': MetricsActivationScene,
|
||||
'time-tracking': TimeTrackingScene,
|
||||
analytics: AnalyticsScene,
|
||||
'search-ai': SearchAiScene,
|
||||
montage: MontageScene,
|
||||
'end-card': EndCardScene
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<TrailerConfigProvider layout="landscape" effects={effects}>
|
||||
<AbsoluteFill>
|
||||
{includeAudio ? <TrailerAudio /> : null}
|
||||
{sceneTimings.map((scene) => {
|
||||
const SceneComponent = sceneMap[scene.id];
|
||||
return (
|
||||
<Sequence key={scene.id} from={scene.from} durationInFrames={scene.duration}>
|
||||
<SceneComponent />
|
||||
</Sequence>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
</TrailerConfigProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import {Audio, interpolate, staticFile, useCurrentFrame} from 'remotion';
|
||||
import {TRAILER_DURATION} from './data/trailerData';
|
||||
|
||||
export const TrailerAudio = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const fadeIn = interpolate(frame, [0, 24], [0, 0.48], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
const fadeOut = interpolate(frame, [TRAILER_DURATION - 36, TRAILER_DURATION], [1, 0], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
|
||||
return <Audio src={staticFile('audio/trailer-bed.mp3')} volume={fadeIn * fadeOut} />;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import type {ComponentType} from 'react';
|
||||
import {AbsoluteFill, Sequence} from 'remotion';
|
||||
import {sceneTimings} from './data/trailerData';
|
||||
import type {SceneTiming} from './data/types';
|
||||
import {TrailerConfigProvider, type TrailerEffects} from './lib/trailer-config';
|
||||
import {TrailerAudio} from './TrailerAudio';
|
||||
import {PortraitAnalyticsScene} from './scenes/portrait/PortraitAnalyticsScene';
|
||||
import {PortraitColdOpenScene} from './scenes/portrait/PortraitColdOpenScene';
|
||||
import {PortraitDashboardScene} from './scenes/portrait/PortraitDashboardScene';
|
||||
import {PortraitMetricsScene} from './scenes/portrait/PortraitMetricsScene';
|
||||
import {PortraitMontageScene} from './scenes/portrait/PortraitMontageScene';
|
||||
import {PortraitSearchAiScene} from './scenes/portrait/PortraitSearchAiScene';
|
||||
import {PortraitTimeTrackingScene} from './scenes/portrait/PortraitTimeTrackingScene';
|
||||
import {EndCardScene} from './scenes/EndCardScene';
|
||||
|
||||
interface VerticalTrailerProps {
|
||||
effects?: TrailerEffects;
|
||||
includeAudio?: boolean;
|
||||
}
|
||||
|
||||
export const VerticalTrailer = ({effects = 'full', includeAudio = true}: VerticalTrailerProps) => {
|
||||
const sceneMap: Record<SceneTiming['id'], ComponentType> = {
|
||||
'cold-open': PortraitColdOpenScene,
|
||||
'dashboard-flyby': PortraitDashboardScene,
|
||||
'metric-activation': PortraitMetricsScene,
|
||||
'time-tracking': PortraitTimeTrackingScene,
|
||||
analytics: PortraitAnalyticsScene,
|
||||
'search-ai': PortraitSearchAiScene,
|
||||
montage: PortraitMontageScene,
|
||||
'end-card': EndCardScene
|
||||
};
|
||||
|
||||
return (
|
||||
<TrailerConfigProvider layout="portrait" effects={effects}>
|
||||
<AbsoluteFill>
|
||||
{includeAudio ? <TrailerAudio /> : null}
|
||||
{sceneTimings.map((scene) => {
|
||||
const SceneComponent = sceneMap[scene.id];
|
||||
return (
|
||||
<Sequence key={scene.id} from={scene.from} durationInFrames={scene.duration}>
|
||||
<SceneComponent />
|
||||
</Sequence>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
</TrailerConfigProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {CSSProperties, ReactNode} from 'react';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
import {HeaderShell} from './HeaderShell';
|
||||
import {SidebarShell} from './SidebarShell';
|
||||
|
||||
interface AppChromeProps {
|
||||
frame: number;
|
||||
activeNav: string;
|
||||
sidebarItems: string[];
|
||||
searchText?: string;
|
||||
searchDelay?: number;
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
contentStyle?: CSSProperties;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showSidebar?: boolean;
|
||||
}
|
||||
|
||||
export const AppChrome = ({
|
||||
frame,
|
||||
activeNav,
|
||||
sidebarItems,
|
||||
searchText,
|
||||
searchDelay = 0,
|
||||
children,
|
||||
style,
|
||||
contentStyle,
|
||||
width = 1520,
|
||||
height = 860,
|
||||
showSidebar = true
|
||||
}: AppChromeProps) => {
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
borderRadius: 28,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(9,11,16,0.82)',
|
||||
boxShadow: isLite ? '0 12px 38px rgba(0, 0, 0, 0.24)' : trackeepTheme.shadow.soft,
|
||||
display: 'flex',
|
||||
backdropFilter: isLite ? 'none' : 'blur(22px)',
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{showSidebar ? <SidebarShell frame={frame} items={sidebarItems} active={activeNav} /> : null}
|
||||
<div style={{flex: 1, display: 'flex', flexDirection: 'column', background: 'rgba(10,12,18,0.55)'}}>
|
||||
<HeaderShell frame={frame} searchText={searchText} searchDelay={searchDelay} />
|
||||
<div style={{flex: 1, padding: '28px 28px 30px', position: 'relative', ...contentStyle}}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface BackdropProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Backdrop = ({children}: BackdropProps) => {
|
||||
const {effects, layout} = useTrailerConfig();
|
||||
const frame = useCurrentFrame();
|
||||
const {durationInFrames} = useVideoConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const introFade = interpolate(frame, [0, 5, 14], [0.92, 0.52, 0], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
const outroFade = interpolate(frame, [durationInFrames - 18, durationInFrames - 7, durationInFrames - 1], [0, 0.4, 0.86], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: layout === 'portrait'
|
||||
? `radial-gradient(circle at 50% 9%, rgba(57,185,255,0.2) 0%, rgba(57,185,255,0) 30%), radial-gradient(circle at 14% 88%, rgba(57,185,255,0.08) 0%, rgba(57,185,255,0) 30%), linear-gradient(180deg, #07090d 0%, ${trackeepTheme.colors.backgroundDeep} 34%, #06080d 100%)`
|
||||
: `radial-gradient(circle at 78% 9%, rgba(57,185,255,0.18) 0%, rgba(57,185,255,0) 30%), radial-gradient(circle at 16% 86%, rgba(57,185,255,0.08) 0%, rgba(57,185,255,0) 30%), linear-gradient(180deg, #07090d 0%, ${trackeepTheme.colors.backgroundDeep} 36%, #06080d 100%)`
|
||||
}}
|
||||
>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(rgba(255,255,255,0.028) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.028) 1px, transparent 1px)',
|
||||
backgroundSize: isLite ? '92px 92px' : '72px 72px',
|
||||
opacity: isLite ? 0.14 : 0.22,
|
||||
maskImage: 'linear-gradient(180deg, rgba(0,0,0,0.92), rgba(0,0,0,0.36))'
|
||||
}}
|
||||
/>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundImage: 'radial-gradient(circle at 20% 20%, rgba(255,255,255,0.05) 0, rgba(255,255,255,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '28px 28px',
|
||||
opacity: isLite ? 0.02 : 0.08,
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: 'radial-gradient(circle at 50% 44%, rgba(0,0,0,0) 40%, rgba(0,0,0,0.54) 100%)',
|
||||
opacity: isLite ? 0.58 : 0.72,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, rgba(2,4,7,0.56) 0%, rgba(2,4,7,0.1) 25%, rgba(2,4,7,0.08) 65%, rgba(2,4,7,0.72) 100%)',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: '#020305',
|
||||
opacity: introFade + outroFade,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface ChartBarsProps {
|
||||
frame: number;
|
||||
delay?: number;
|
||||
values: Array<{label: string; value: number}>;
|
||||
}
|
||||
|
||||
export const ChartBars = ({frame, delay = 0, values}: ChartBarsProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const entrance = springIn(frame, fps, delay, 26);
|
||||
const maxValue = Math.max(...values.map((entry) => entry.value));
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 22,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
padding: '22px 24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 18,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [26, 0])}px, 0)`
|
||||
}}
|
||||
>
|
||||
<div style={{fontSize: 26, fontWeight: 700, color: trackeepTheme.colors.text, letterSpacing: '-0.04em'}}>Weekly Activity</div>
|
||||
<div style={{display: 'flex', alignItems: 'flex-end', gap: 16, height: 180}}>
|
||||
{values.map((entry, index) => {
|
||||
const progress = springIn(frame, fps, delay + 10 + index * 4, 24);
|
||||
const height = interpolate(progress, [0, 1], [0, (entry.value / maxValue) * 128]);
|
||||
|
||||
return (
|
||||
<div key={entry.label + index} style={{flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10}}>
|
||||
<div style={{fontSize: 16, color: trackeepTheme.colors.textMuted}}>{entry.value}</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 48,
|
||||
height,
|
||||
borderRadius: 18,
|
||||
background: `linear-gradient(180deg, ${trackeepTheme.colors.primary} 0%, rgba(57,185,255,0.25) 100%)`,
|
||||
boxShadow: `0 0 22px ${trackeepTheme.colors.primarySoft}`
|
||||
}}
|
||||
/>
|
||||
<div style={{fontSize: 18, fontWeight: 600, color: trackeepTheme.colors.textMuted}}>{entry.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import type {CSSProperties, ReactNode} from 'react';
|
||||
import {Img, interpolate, staticFile} from 'remotion';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface MotionRange {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
interface DemoSurfaceProps {
|
||||
frame: number;
|
||||
src: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
style?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
rotateX?: MotionRange;
|
||||
rotateY?: MotionRange;
|
||||
rotateZ?: MotionRange;
|
||||
scale?: MotionRange;
|
||||
translateX?: MotionRange;
|
||||
translateY?: MotionRange;
|
||||
imageScale?: MotionRange;
|
||||
imageX?: MotionRange;
|
||||
imageY?: MotionRange;
|
||||
}
|
||||
|
||||
const resolveMotion = (frame: number, motion: MotionRange | undefined, fallback: number) => {
|
||||
if (!motion) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return interpolate(frame, [0, 120], [motion.from, motion.to], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
};
|
||||
|
||||
export const DemoSurface = ({
|
||||
frame,
|
||||
src,
|
||||
width = 1520,
|
||||
height = 855,
|
||||
style,
|
||||
children,
|
||||
rotateX,
|
||||
rotateY,
|
||||
rotateZ,
|
||||
scale,
|
||||
translateX,
|
||||
translateY,
|
||||
imageScale,
|
||||
imageX,
|
||||
imageY
|
||||
}: DemoSurfaceProps) => {
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const resolvedRotateX = resolveMotion(frame, rotateX, 0);
|
||||
const resolvedRotateY = resolveMotion(frame, rotateY, 0);
|
||||
const resolvedRotateZ = resolveMotion(frame, rotateZ, 0);
|
||||
const resolvedScale = resolveMotion(frame, scale, 1);
|
||||
const resolvedTranslateX = resolveMotion(frame, translateX, 0);
|
||||
const resolvedTranslateY = resolveMotion(frame, translateY, 0);
|
||||
const resolvedImageScale = resolveMotion(frame, imageScale, 1);
|
||||
const resolvedImageX = resolveMotion(frame, imageX, 0);
|
||||
const resolvedImageY = resolveMotion(frame, imageY, 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width,
|
||||
height,
|
||||
borderRadius: 32,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: trackeepTheme.colors.backgroundDeep,
|
||||
boxShadow: isLite
|
||||
? '0 24px 70px rgba(0, 0, 0, 0.34), 0 0 0 1px rgba(255,255,255,0.03)'
|
||||
: '0 46px 140px rgba(0, 0, 0, 0.58), 0 22px 62px rgba(0, 0, 0, 0.42), 0 0 0 1px rgba(255,255,255,0.04)',
|
||||
transform: `perspective(2200px) rotateX(${resolvedRotateX}deg) rotateY(${resolvedRotateY}deg) rotateZ(${resolvedRotateZ}deg) translate3d(${resolvedTranslateX}px, ${resolvedTranslateY}px, 0) scale(${resolvedScale})`,
|
||||
transformOrigin: 'center center',
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile(src)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
transform: `translate3d(${resolvedImageX}px, ${resolvedImageY}px, 0) scale(${resolvedImageScale})`
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(180deg, rgba(5,7,10,0.14) 0%, rgba(5,7,10,0) 28%, rgba(5,7,10,0.3) 100%)'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(90deg, rgba(4,6,9,0.34) 0%, rgba(4,6,9,0) 22%, rgba(4,6,9,0) 78%, rgba(4,6,9,0.38) 100%)'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.04), inset 0 -64px 82px rgba(0,0,0,0.2)',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import trackeepLogo from '../../../frontend/public/trackeep.svg';
|
||||
import {Img, interpolate, useVideoConfig} from 'remotion';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface EndCardProps {
|
||||
frame: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export const EndCard = ({frame, title, subtitle}: EndCardProps) => {
|
||||
const {fps, durationInFrames} = useVideoConfig();
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const entrance = springIn(frame, fps, 0, 24);
|
||||
const logoEntrance = springIn(frame, fps, 4, 22);
|
||||
const titleEntrance = springIn(frame, fps, 8, 20);
|
||||
const underline = springIn(frame, fps, 12, 20);
|
||||
const subtitleEntrance = springIn(frame, fps, 16, 20);
|
||||
const outroFade = interpolate(frame, [durationInFrames - 18, durationInFrames - 7, durationInFrames - 1], [0, 0.3, 0.8], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 30,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [26, 0])}px, 0)`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
width: 820,
|
||||
height: 480,
|
||||
borderRadius: 999,
|
||||
transform: `translate(-50%, -48%) scale(${interpolate(logoEntrance, [0, 1], [0.9, 1.05])})`,
|
||||
background: 'radial-gradient(circle, rgba(57,185,255,0.16) 0%, rgba(57,185,255,0.02) 42%, rgba(57,185,255,0) 75%)',
|
||||
opacity: logoEntrance
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 154,
|
||||
height: 154,
|
||||
borderRadius: 40,
|
||||
background: 'rgba(57,185,255,0.08)',
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: logoEntrance,
|
||||
transform: `scale(${interpolate(logoEntrance, [0, 1], [0.92, 1])})`,
|
||||
boxShadow: isLite ? '0 0 30px rgba(57,185,255,0.12)' : `0 0 100px ${trackeepTheme.colors.primarySoft}, 0 28px 80px rgba(0,0,0,0.34)`
|
||||
}}
|
||||
>
|
||||
<Img src={trackeepLogo} style={{width: 86, height: 86}} />
|
||||
</div>
|
||||
<div style={{fontSize: 16, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: trackeepTheme.colors.primary, opacity: subtitleEntrance}}>
|
||||
Plan • Track • Learn
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 124,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.08em',
|
||||
lineHeight: 0.9,
|
||||
color: trackeepTheme.colors.text,
|
||||
opacity: titleEntrance,
|
||||
transform: `translate3d(0, ${interpolate(titleEntrance, [0, 1], [18, 0])}px, 0)`
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: interpolate(underline, [0, 1], [0, 320]),
|
||||
height: 6,
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
background: `linear-gradient(90deg, ${trackeepTheme.colors.primary} 0%, rgba(57,185,255,0) 100%)`
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 980,
|
||||
textAlign: 'center',
|
||||
fontSize: 30,
|
||||
lineHeight: 1.34,
|
||||
color: trackeepTheme.colors.textMuted,
|
||||
letterSpacing: '-0.03em',
|
||||
opacity: subtitleEntrance
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: '#020305',
|
||||
opacity: outroFade
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import {IconChevronDown, IconLink, IconMenu2, IconUpload} from '@tabler/icons-react';
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
import {SearchField} from './SearchField';
|
||||
|
||||
interface HeaderShellProps {
|
||||
frame: number;
|
||||
searchText?: string;
|
||||
searchDelay?: number;
|
||||
}
|
||||
|
||||
const controlButtonStyle = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
} as const;
|
||||
|
||||
export const HeaderShell = ({frame, searchText, searchDelay = 0}: HeaderShellProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const entrance = springIn(frame, fps, 6, 22);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 78,
|
||||
borderBottom: `1px solid ${trackeepTheme.colors.border}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '14px 18px 14px 22px',
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [-14, 0])}px, 0)`
|
||||
}}
|
||||
>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 16}}>
|
||||
<div style={{width: 34, height: 34, display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||
<IconMenu2 size={22} stroke={1.9} color={trackeepTheme.colors.text} />
|
||||
</div>
|
||||
<SearchField frame={frame} delay={searchDelay} query={searchText} />
|
||||
</div>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 10}}>
|
||||
<div
|
||||
style={{
|
||||
height: 44,
|
||||
padding: '0 18px',
|
||||
borderRadius: 10,
|
||||
background: trackeepTheme.colors.primary,
|
||||
color: '#03131d',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
fontSize: 18,
|
||||
fontWeight: 700,
|
||||
boxShadow: `0 0 28px ${trackeepTheme.colors.primarySoft}`
|
||||
}}
|
||||
>
|
||||
<IconUpload size={16} stroke={2} />
|
||||
Import a document
|
||||
</div>
|
||||
<div style={controlButtonStyle}>
|
||||
<IconLink size={16} stroke={1.9} color={trackeepTheme.colors.textMuted} />
|
||||
</div>
|
||||
<div style={controlButtonStyle}>
|
||||
<IconChevronDown size={16} stroke={1.9} color={trackeepTheme.colors.textMuted} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 40,
|
||||
padding: '0 12px',
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
background: trackeepTheme.colors.primary,
|
||||
color: '#03131d',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
letterSpacing: '-0.03em'
|
||||
}}
|
||||
>
|
||||
DU
|
||||
</div>
|
||||
<IconChevronDown size={14} stroke={1.9} color={trackeepTheme.colors.textMuted} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import {interpolate} from 'remotion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface HighlightBoxProps {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
progress: number;
|
||||
label: string;
|
||||
align?: 'top-left' | 'top-right' | 'bottom-left';
|
||||
}
|
||||
|
||||
export const HighlightBox = ({x, y, width, height, progress, label, align = 'top-left'}: HighlightBoxProps) => {
|
||||
const badgeLeft = align === 'top-right' ? width - 12 : 16;
|
||||
const badgeTop = align === 'bottom-left' ? height + 12 : -18;
|
||||
const badgeTransform = align === 'top-right' ? 'translateX(-100%)' : 'none';
|
||||
const translateY = interpolate(progress, [0, 1], [18, 0]);
|
||||
const scale = interpolate(progress, [0, 1], [0.96, 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x,
|
||||
top: y,
|
||||
width,
|
||||
height,
|
||||
borderRadius: 24,
|
||||
opacity: progress,
|
||||
transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`,
|
||||
border: `1px solid rgba(57, 185, 255, 0.92)`,
|
||||
boxShadow: `inset 0 0 0 1px rgba(255,255,255,0.06), 0 0 0 1px rgba(57,185,255,0.1), 0 0 42px ${trackeepTheme.colors.primarySoft}`,
|
||||
background: 'linear-gradient(180deg, rgba(57,185,255,0.12) 0%, rgba(57,185,255,0.02) 100%)',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: badgeLeft,
|
||||
top: badgeTop,
|
||||
transform: badgeTransform,
|
||||
padding: '8px 12px',
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
background: 'rgba(8, 14, 22, 0.88)',
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
color: trackeepTheme.colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
boxShadow: '0 10px 26px rgba(0, 0, 0, 0.26)'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {ComponentType} from 'react';
|
||||
import {
|
||||
IconActivity,
|
||||
IconBookmark,
|
||||
IconBrandGithub,
|
||||
IconChecklist,
|
||||
IconClock,
|
||||
IconDatabase,
|
||||
IconFileText,
|
||||
IconFlame,
|
||||
IconFolder,
|
||||
IconHome,
|
||||
IconMessageCircle,
|
||||
IconNotes,
|
||||
IconSchool,
|
||||
IconSearch,
|
||||
IconSparkles,
|
||||
IconVideo
|
||||
} from '@tabler/icons-react';
|
||||
import type {ShowcaseIcon} from '../data/types';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
const iconMap: Record<ShowcaseIcon, ComponentType<{size?: number; stroke?: number; color?: string}>> = {
|
||||
home: IconHome,
|
||||
documents: IconFileText,
|
||||
bookmarks: IconBookmark,
|
||||
tasks: IconChecklist,
|
||||
notes: IconNotes,
|
||||
videos: IconVideo,
|
||||
learning: IconSchool,
|
||||
time: IconClock,
|
||||
productivity: IconActivity,
|
||||
storage: IconDatabase,
|
||||
analytics: IconActivity,
|
||||
github: IconBrandGithub,
|
||||
search: IconSearch,
|
||||
ai: IconSparkles,
|
||||
messages: IconMessageCircle,
|
||||
files: IconFolder,
|
||||
habit: IconFlame
|
||||
};
|
||||
|
||||
interface IconBadgeProps {
|
||||
icon: ShowcaseIcon;
|
||||
size?: number;
|
||||
accent?: string;
|
||||
}
|
||||
|
||||
export const IconBadge = ({icon, size = 16, accent = trackeepTheme.colors.primary}: IconBadgeProps) => {
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const Icon = iconMap[icon];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size + 14,
|
||||
height: size + 14,
|
||||
borderRadius: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
boxShadow: isLite ? '0 0 0 1px rgba(255,255,255,0.02)' : `0 0 0 1px rgba(255,255,255,0.02), 0 0 18px ${trackeepTheme.colors.primarySoft}`
|
||||
}}
|
||||
>
|
||||
<Icon size={size} stroke={1.8} color={accent} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import type {CSSProperties} from 'react';
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import type {HeadlineBeat} from '../data/types';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface KineticHeadlineProps {
|
||||
frame: number;
|
||||
beat: HeadlineBeat;
|
||||
style?: CSSProperties;
|
||||
titleSize?: number;
|
||||
subtitleSize?: number;
|
||||
kickerSize?: number;
|
||||
maxWidth?: number | string;
|
||||
}
|
||||
|
||||
export const KineticHeadline = ({
|
||||
frame,
|
||||
beat,
|
||||
style,
|
||||
titleSize,
|
||||
subtitleSize = 26,
|
||||
kickerSize = 16,
|
||||
maxWidth
|
||||
}: KineticHeadlineProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const entrance = springIn(frame, fps, 0, 34);
|
||||
const align = beat.align ?? 'left';
|
||||
const centered = align === 'center';
|
||||
const resolvedTitleSize = titleSize ?? (centered ? 94 : 84);
|
||||
const resolvedMaxWidth = maxWidth ?? (centered ? '100%' : 640);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 14,
|
||||
width: centered ? 1120 : 760,
|
||||
textAlign: align,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [36, 0])}px, 0)`,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{beat.kicker ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: kickerSize,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
color: trackeepTheme.colors.primary
|
||||
}}
|
||||
>
|
||||
{beat.kicker}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
fontSize: resolvedTitleSize,
|
||||
lineHeight: 0.96,
|
||||
letterSpacing: '-0.05em',
|
||||
fontWeight: 700,
|
||||
color: trackeepTheme.colors.text,
|
||||
maxWidth: resolvedMaxWidth
|
||||
}}
|
||||
>
|
||||
{beat.title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: centered ? 260 : 180,
|
||||
height: 4,
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
background: `linear-gradient(90deg, ${trackeepTheme.colors.primary} 0%, rgba(57,185,255,0) 100%)`,
|
||||
alignSelf: centered ? 'center' : 'flex-start'
|
||||
}}
|
||||
/>
|
||||
{beat.subtitle ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: subtitleSize,
|
||||
lineHeight: 1.35,
|
||||
color: trackeepTheme.colors.textMuted,
|
||||
maxWidth: centered ? 760 : 620
|
||||
}}
|
||||
>
|
||||
{beat.subtitle}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import type {ShowcaseIcon} from '../data/types';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
import {IconBadge} from './IconBadge';
|
||||
|
||||
interface MetricTileProps {
|
||||
frame: number;
|
||||
delay?: number;
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ShowcaseIcon;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const MetricTile = ({frame, delay = 0, label, value, icon, compact = false}: MetricTileProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const entrance = springIn(frame, fps, delay, 22);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: compact ? 16 : 18,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.018), rgba(255,255,255,0.01))',
|
||||
padding: compact ? '16px 18px' : '20px 22px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
gap: compact ? 10 : 16,
|
||||
minHeight: compact ? 104 : 148,
|
||||
boxShadow: isLite ? '0 8px 16px rgba(0, 0, 0, 0.14)' : trackeepTheme.shadow.card,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [24, 0])}px, 0) scale(${interpolate(entrance, [0, 1], [0.96, 1])})`,
|
||||
filter: isLite ? 'none' : `blur(${interpolate(entrance, [0, 1], [8, 0])}px)`
|
||||
}}
|
||||
>
|
||||
<IconBadge icon={icon} size={compact ? 14 : 16} />
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 6}}>
|
||||
<div style={{fontSize: compact ? 32 : 48, letterSpacing: '-0.05em', fontWeight: 700, color: trackeepTheme.colors.text}}>{value}</div>
|
||||
<div style={{fontSize: compact ? 17 : 21, color: trackeepTheme.colors.textMuted, fontWeight: 500}}>{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import type {ProgressBeat} from '../data/types';
|
||||
import {easedProgress, springIn} from '../lib/motion';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
import {IconBadge} from './IconBadge';
|
||||
|
||||
interface ProgressModuleProps {
|
||||
frame: number;
|
||||
delay?: number;
|
||||
beat: ProgressBeat;
|
||||
}
|
||||
|
||||
export const ProgressModule = ({frame, delay = 0, beat}: ProgressModuleProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const entrance = springIn(frame, fps, delay, 28);
|
||||
const fill = easedProgress(frame, delay + 14, 48) * beat.progress;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 22,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
padding: '22px 24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 18,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [30, 0])}px, 0)`
|
||||
}}
|
||||
>
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 14}}>
|
||||
<IconBadge icon={beat.icon} />
|
||||
<div>
|
||||
<div style={{fontSize: 28, fontWeight: 700, letterSpacing: '-0.04em', color: trackeepTheme.colors.text}}>
|
||||
{beat.label}
|
||||
</div>
|
||||
{beat.caption ? (
|
||||
<div style={{fontSize: 18, color: trackeepTheme.colors.textMuted, marginTop: 4}}>{beat.caption}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{fontSize: 30, fontWeight: 700, color: trackeepTheme.colors.text}}>{beat.valueLabel}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 12,
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
overflow: 'hidden',
|
||||
background: 'rgba(255,255,255,0.08)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${fill * 100}%`,
|
||||
height: '100%',
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
background: `linear-gradient(90deg, ${trackeepTheme.colors.primary} 0%, rgba(57,185,255,0.52) 100%)`,
|
||||
boxShadow: isLite ? 'none' : `0 0 20px ${trackeepTheme.colors.primaryGlow}`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import {interpolate, staticFile} from 'remotion';
|
||||
import type {RoutePreviewBeat} from '../data/types';
|
||||
import {IconBadge} from './IconBadge';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface RoutePreviewCardProps {
|
||||
beat: RoutePreviewBeat;
|
||||
progress: number;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
emphasis?: boolean;
|
||||
}
|
||||
|
||||
export const RoutePreviewCard = ({beat, progress, width, height, x, y, emphasis = false}: RoutePreviewCardProps) => {
|
||||
const translateY = interpolate(progress, [0, 1], [22, 0]);
|
||||
const scale = interpolate(progress, [0, 1], [0.93, 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x,
|
||||
top: y,
|
||||
width,
|
||||
height,
|
||||
borderRadius: emphasis ? 30 : 26,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(7, 10, 15, 0.9)',
|
||||
opacity: progress,
|
||||
transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`,
|
||||
boxShadow: emphasis ? '0 36px 120px rgba(0, 0, 0, 0.52)' : '0 24px 70px rgba(0, 0, 0, 0.38)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: Math.round(height * 0.72),
|
||||
backgroundImage: `url(${staticFile(beat.image)})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
borderBottom: `1px solid ${trackeepTheme.colors.border}`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(180deg, rgba(5,7,10,0.1) 0%, rgba(5,7,10,0) 42%, rgba(5,7,10,0.52) 100%)'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 14,
|
||||
top: 14,
|
||||
padding: '6px 10px',
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(4, 8, 14, 0.84)',
|
||||
color: trackeepTheme.colors.text,
|
||||
fontSize: emphasis ? 12 : 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
{beat.label}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{padding: emphasis ? '22px 24px 24px' : '16px 18px 18px', display: 'flex', flexDirection: 'column', gap: emphasis ? 12 : 10}}>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 10}}>
|
||||
<IconBadge icon={beat.icon} size={15} />
|
||||
<div style={{fontSize: emphasis ? 24 : 19, fontWeight: 700, color: trackeepTheme.colors.text, letterSpacing: '-0.045em'}}>{beat.title}</div>
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 6}}>
|
||||
{beat.lines.map((line) => (
|
||||
<div key={line} style={{fontSize: emphasis ? 15 : 13, lineHeight: 1.38, color: trackeepTheme.colors.textMuted}}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import {IconSearch} from '@tabler/icons-react';
|
||||
import {easedProgress, springIn} from '../lib/motion';
|
||||
import {useTrailerConfig} from '../lib/trailer-config';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface SearchFieldProps {
|
||||
frame: number;
|
||||
query?: string;
|
||||
delay?: number;
|
||||
placeholder?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const SearchField = ({
|
||||
frame,
|
||||
query,
|
||||
delay = 0,
|
||||
placeholder = 'Quick search',
|
||||
width = 320
|
||||
}: SearchFieldProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const {effects} = useTrailerConfig();
|
||||
const isLite = effects === 'lite';
|
||||
const entrance = springIn(frame, fps, delay, 24);
|
||||
const typing = easedProgress(frame, delay + 10, 54);
|
||||
const typedLength = query ? Math.max(0, Math.floor(query.length * typing)) : 0;
|
||||
const typedQuery = query ? query.slice(0, typedLength) : '';
|
||||
const displayText = typedQuery || placeholder;
|
||||
const isPlaceholder = typedLength === 0;
|
||||
const cursorOpacity = Math.sin(frame / 3) > 0 ? 1 : 0.2;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height: 44,
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(10,12,16,0.66)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '0 14px',
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [14, 0])}px, 0) scale(${interpolate(entrance, [0, 1], [0.97, 1])})`,
|
||||
boxShadow: isLite ? 'none' : 'inset 0 1px 0 rgba(255,255,255,0.03)'
|
||||
}}
|
||||
>
|
||||
<IconSearch size={16} stroke={1.9} color={trackeepTheme.colors.textMuted} />
|
||||
<div
|
||||
style={{
|
||||
color: isPlaceholder ? trackeepTheme.colors.textMuted : trackeepTheme.colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '-0.02em',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
{query ? <span style={{opacity: cursorOpacity, color: trackeepTheme.colors.primary, marginLeft: 2}}>|</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import trackeepLogo from '../../../frontend/public/trackeep.svg';
|
||||
import {
|
||||
IconBookmark,
|
||||
IconBrandGithub,
|
||||
IconCalendar,
|
||||
IconChartLine,
|
||||
IconChecklist,
|
||||
IconClock,
|
||||
IconFolder,
|
||||
IconHome,
|
||||
IconMessageCircle,
|
||||
IconNotes,
|
||||
IconSchool,
|
||||
IconSparkles,
|
||||
IconUsers,
|
||||
IconVideo
|
||||
} from '@tabler/icons-react';
|
||||
import {Img, interpolate, useVideoConfig} from 'remotion';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface SidebarShellProps {
|
||||
frame: number;
|
||||
items: string[];
|
||||
active: string;
|
||||
}
|
||||
|
||||
const navIconMap = {
|
||||
Home: IconHome,
|
||||
Bookmarks: IconBookmark,
|
||||
Tasks: IconChecklist,
|
||||
'Time Tracking': IconClock,
|
||||
Calendar: IconCalendar,
|
||||
Files: IconFolder,
|
||||
Notes: IconNotes,
|
||||
Messages: IconMessageCircle,
|
||||
YouTube: IconVideo,
|
||||
Members: IconUsers,
|
||||
Learning: IconSchool,
|
||||
Stats: IconChartLine,
|
||||
GitHub: IconBrandGithub,
|
||||
'AI Assistant': IconSparkles
|
||||
} as const;
|
||||
|
||||
export const SidebarShell = ({frame, items, active}: SidebarShellProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const entrance = springIn(frame, fps, 0, 28);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 278,
|
||||
borderRight: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(7,8,12,0.76)',
|
||||
padding: '18px 14px 18px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 14,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(${interpolate(entrance, [0, 1], [-24, 0])}px, 0, 0)`
|
||||
}}
|
||||
>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 12, padding: '4px 8px 10px'}}>
|
||||
<Img src={trackeepLogo} style={{width: 22, height: 22}} />
|
||||
<div style={{fontSize: 18, fontWeight: 700, letterSpacing: '-0.03em', color: trackeepTheme.colors.text}}>Trackeep</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 14px',
|
||||
color: trackeepTheme.colors.text,
|
||||
fontSize: 17,
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
<span>Trackeep Workspace</span>
|
||||
<span style={{color: trackeepTheme.colors.textMuted}}>⌄</span>
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 4}}>
|
||||
{items.map((item, index) => {
|
||||
const isActive = item === active;
|
||||
const Icon = navIconMap[item as keyof typeof navIconMap] ?? IconFolder;
|
||||
return (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '0 12px',
|
||||
fontSize: 16,
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
color: isActive ? trackeepTheme.colors.text : trackeepTheme.colors.textMuted,
|
||||
background: isActive ? 'rgba(255,255,255,0.08)' : 'transparent',
|
||||
border: isActive ? `1px solid rgba(255,255,255,0.04)` : '1px solid transparent',
|
||||
opacity: springIn(frame, fps, 8 + index * 2, 18)
|
||||
}}
|
||||
>
|
||||
<Icon size={16} stroke={1.9} color={isActive ? trackeepTheme.colors.primary : trackeepTheme.colors.textMuted} />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import {interpolate} from 'remotion';
|
||||
import type {ShowcaseIcon} from '../data/types';
|
||||
import {IconBadge} from './IconBadge';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
interface SignalBadgeProps {
|
||||
x: number;
|
||||
y: number;
|
||||
progress: number;
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ShowcaseIcon;
|
||||
}
|
||||
|
||||
export const SignalBadge = ({x, y, progress, label, value, icon}: SignalBadgeProps) => {
|
||||
const translateY = interpolate(progress, [0, 1], [18, 0]);
|
||||
const scale = interpolate(progress, [0, 1], [0.92, 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x,
|
||||
top: y,
|
||||
minWidth: 180,
|
||||
padding: '14px 16px',
|
||||
borderRadius: 20,
|
||||
background: 'rgba(8, 12, 18, 0.88)',
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
opacity: progress,
|
||||
transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`,
|
||||
boxShadow: '0 18px 42px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
>
|
||||
<IconBadge icon={icon} size={16} />
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
<div style={{fontSize: 28, fontWeight: 700, color: trackeepTheme.colors.text, letterSpacing: '-0.05em'}}>{value}</div>
|
||||
<div style={{fontSize: 14, fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: trackeepTheme.colors.textMuted}}>{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import {interpolate, useVideoConfig} from 'remotion';
|
||||
import {easedProgress, springIn} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
import {IconBadge} from './IconBadge';
|
||||
|
||||
interface TypingMessageProps {
|
||||
frame: number;
|
||||
delay?: number;
|
||||
text: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export const TypingMessage = ({frame, delay = 0, text, timestamp = '08:25 AM'}: TypingMessageProps) => {
|
||||
const {fps} = useVideoConfig();
|
||||
const entrance = springIn(frame, fps, delay, 28);
|
||||
const typingProgress = easedProgress(frame, delay + 18, 86);
|
||||
const visibleLength = Math.max(0, Math.floor(text.length * typingProgress));
|
||||
const visibleText = text.slice(0, visibleLength);
|
||||
const cursorOpacity = Math.sin(frame / 3) > 0 ? 1 : 0.2;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 24,
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
padding: '22px 24px',
|
||||
display: 'flex',
|
||||
gap: 18,
|
||||
opacity: entrance,
|
||||
transform: `translate3d(0, ${interpolate(entrance, [0, 1], [24, 0])}px, 0)`
|
||||
}}
|
||||
>
|
||||
<IconBadge icon="ai" size={22} />
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 12, flex: 1}}>
|
||||
<div style={{fontSize: 28, lineHeight: 1.3, color: trackeepTheme.colors.text, letterSpacing: '-0.03em', minHeight: 80}}>
|
||||
{visibleText}
|
||||
<span style={{opacity: cursorOpacity, color: trackeepTheme.colors.primary}}>|</span>
|
||||
</div>
|
||||
<div style={{fontSize: 18, color: trackeepTheme.colors.textMuted}}>{timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import './styles/global.css';
|
||||
import {registerRoot} from 'remotion';
|
||||
import {RemotionRoot} from './Root';
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
@@ -0,0 +1,28 @@
|
||||
import type {MetricCard} from '../data/types';
|
||||
|
||||
export const formatMetricValue = (metric: MetricCard, value: number): string => {
|
||||
const prefix = metric.prefix ?? '';
|
||||
const suffix = metric.suffix ?? '';
|
||||
|
||||
if (metric.decimals !== undefined) {
|
||||
return `${prefix}${value.toFixed(metric.decimals)}${suffix}`;
|
||||
}
|
||||
|
||||
return `${prefix}${Math.round(value).toLocaleString('en-US')}${suffix}`;
|
||||
};
|
||||
|
||||
export const formatStopwatch = (seconds: number): string => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainder = seconds % 60;
|
||||
return `${String(minutes).padStart(2, '0')}:${String(remainder).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export const formatAnalyticsHours = (hours: number): string => {
|
||||
const wholeHours = Math.floor(hours);
|
||||
const minutes = Math.round((hours - wholeHours) * 60);
|
||||
return `${wholeHours}h ${String(minutes).padStart(2, '0')}m`;
|
||||
};
|
||||
|
||||
export const clamp = (value: number, min = 0, max = 1): number => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type {CSSProperties} from 'react';
|
||||
import {Easing, interpolate, spring} from 'remotion';
|
||||
|
||||
export const springIn = (frame: number, fps: number, delay = 0, durationInFrames = 30): number => {
|
||||
return spring({
|
||||
frame: Math.max(0, frame - delay),
|
||||
fps,
|
||||
durationInFrames,
|
||||
config: {
|
||||
damping: 200,
|
||||
stiffness: 140,
|
||||
mass: 0.8
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const easedProgress = (frame: number, start: number, duration: number): number => {
|
||||
return interpolate(frame, [start, start + duration], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
easing: Easing.bezier(0.22, 1, 0.36, 1)
|
||||
});
|
||||
};
|
||||
|
||||
export const fadeSlideUpStyle = (progress: number, distance = 48): CSSProperties => {
|
||||
return {
|
||||
opacity: progress,
|
||||
transform: `translate3d(0, ${interpolate(progress, [0, 1], [distance, 0])}px, 0) scale(${interpolate(progress, [0, 1], [0.96, 1])})`,
|
||||
filter: `blur(${interpolate(progress, [0, 1], [10, 0])}px)`
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type {PropsWithChildren} from 'react';
|
||||
import {createContext, useContext} from 'react';
|
||||
|
||||
export type TrailerLayout = 'landscape' | 'portrait';
|
||||
export type TrailerEffects = 'full' | 'lite';
|
||||
|
||||
interface TrailerConfigValue {
|
||||
layout: TrailerLayout;
|
||||
effects: TrailerEffects;
|
||||
}
|
||||
|
||||
const TrailerConfigContext = createContext<TrailerConfigValue>({
|
||||
layout: 'landscape',
|
||||
effects: 'full'
|
||||
});
|
||||
|
||||
export const TrailerConfigProvider = ({children, layout, effects}: PropsWithChildren<TrailerConfigValue>) => {
|
||||
return (
|
||||
<TrailerConfigContext.Provider value={{layout, effects}}>
|
||||
{children}
|
||||
</TrailerConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTrailerConfig = () => useContext(TrailerConfigContext);
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.css';
|
||||
@@ -0,0 +1,48 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {DemoSurface} from '../components/DemoSurface';
|
||||
import {HighlightBox} from '../components/HighlightBox';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {SignalBadge} from '../components/SignalBadge';
|
||||
import {analyticsHeadline, analyticsSummary, routeScreens} from '../data/trailerData';
|
||||
import {formatAnalyticsHours, formatMetricValue} from '../lib/format';
|
||||
import {easedProgress} from '../lib/motion';
|
||||
|
||||
const SCALE = 1520 / 1280;
|
||||
const s = (value: number) => value * SCALE;
|
||||
|
||||
export const AnalyticsScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const hoursProgress = easedProgress(frame, 8, 28);
|
||||
const tasksProgress = easedProgress(frame, 16, 28);
|
||||
const bookmarksProgress = easedProgress(frame, 24, 28);
|
||||
const commitsProgress = easedProgress(frame, 32, 28);
|
||||
const summaryBox = easedProgress(frame, 14, 18);
|
||||
const habitBox = easedProgress(frame, 38, 18);
|
||||
const learningBox = easedProgress(frame, 58, 18);
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline frame={frame} beat={analyticsHeadline} titleSize={66} subtitleSize={22} style={{left: 120, top: 92, width: 680}} />
|
||||
<SignalBadge x={1050} y={112} progress={hoursProgress} label="Hours" value={formatAnalyticsHours(analyticsSummary[0].target * hoursProgress)} icon="time" />
|
||||
<SignalBadge x={1260} y={112} progress={tasksProgress} label="Tasks" value={formatMetricValue(analyticsSummary[1], analyticsSummary[1].target * tasksProgress)} icon="tasks" />
|
||||
<SignalBadge x={1470} y={112} progress={bookmarksProgress} label="Bookmarks" value={formatMetricValue(analyticsSummary[2], analyticsSummary[2].target * bookmarksProgress)} icon="bookmarks" />
|
||||
<SignalBadge x={1680} y={112} progress={commitsProgress} label="Commits" value={formatMetricValue(analyticsSummary[3], analyticsSummary[3].target * commitsProgress)} icon="github" />
|
||||
<DemoSurface
|
||||
frame={frame}
|
||||
src={routeScreens.analytics}
|
||||
style={{left: 260, top: 186}}
|
||||
rotateX={{from: 4.5, to: 1.25}}
|
||||
rotateY={{from: -4, to: 1.75}}
|
||||
scale={{from: 1.035, to: 1.005}}
|
||||
translateX={{from: 40, to: -12}}
|
||||
translateY={{from: 18, to: -4}}
|
||||
imageScale={{from: 1.03, to: 1.01}}
|
||||
>
|
||||
<HighlightBox x={s(310)} y={s(184)} width={s(938)} height={s(100)} progress={summaryBox} label="Top-line metrics" />
|
||||
<HighlightBox x={s(790)} y={s(310)} width={s(458)} height={s(190)} progress={habitBox} label="Habit tracking" align="top-right" />
|
||||
<HighlightBox x={s(311)} y={s(526)} width={s(936)} height={s(182)} progress={learningBox} label="Learning progress" />
|
||||
</DemoSurface>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import trackeepLogo from '../../../frontend/public/trackeep.svg';
|
||||
import {Img, interpolate, useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {DemoSurface} from '../components/DemoSurface';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {routeScreens} from '../data/trailerData';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
export const ColdOpenScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const logoOpacity = interpolate(frame, [0, 10, 24], [0, 1, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
const sweepWidth = interpolate(frame, [10, 30], [0, 260], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<div style={{position: 'absolute', inset: 0, padding: '118px 140px'}}>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 16, opacity: logoOpacity}}>
|
||||
<Img src={trackeepLogo} style={{width: 36, height: 36}} />
|
||||
<div style={{fontSize: 32, fontWeight: 700, letterSpacing: '-0.05em', color: trackeepTheme.colors.text}}>Trackeep</div>
|
||||
</div>
|
||||
<KineticHeadline
|
||||
frame={frame}
|
||||
beat={{
|
||||
kicker: 'Real Product Capture',
|
||||
title: 'Trackeep, in motion.',
|
||||
subtitle: 'Fast-cut walkthrough of the live interface.',
|
||||
align: 'left'
|
||||
}}
|
||||
titleSize={78}
|
||||
subtitleSize={22}
|
||||
style={{left: 140, top: 288, width: 680}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 140,
|
||||
top: 610,
|
||||
width: sweepWidth,
|
||||
height: 5,
|
||||
borderRadius: trackeepTheme.radius.pill,
|
||||
background: `linear-gradient(90deg, ${trackeepTheme.colors.primary} 0%, rgba(57,185,255,0) 100%)`,
|
||||
boxShadow: `0 0 36px ${trackeepTheme.colors.primaryGlow}`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DemoSurface
|
||||
frame={frame}
|
||||
src={routeScreens.dashboard}
|
||||
width={720}
|
||||
height={405}
|
||||
style={{left: 1080, top: 238, opacity: 0.52, filter: 'blur(0.3px)'}}
|
||||
rotateX={{from: 9, to: 4}}
|
||||
rotateY={{from: -14, to: -8}}
|
||||
scale={{from: 1.06, to: 1.02}}
|
||||
translateX={{from: 42, to: 0}}
|
||||
imageScale={{from: 1.08, to: 1.04}}
|
||||
/>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {DemoSurface} from '../components/DemoSurface';
|
||||
import {HighlightBox} from '../components/HighlightBox';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {SignalBadge} from '../components/SignalBadge';
|
||||
import {dashboardHeadline, routeScreens} from '../data/trailerData';
|
||||
import {easedProgress} from '../lib/motion';
|
||||
|
||||
const SCALE = 1520 / 1280;
|
||||
const s = (value: number) => value * SCALE;
|
||||
|
||||
export const DashboardFlybyScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const docsProgress = easedProgress(frame, 10, 26);
|
||||
const tasksProgress = easedProgress(frame, 18, 28);
|
||||
const productivityProgress = easedProgress(frame, 26, 28);
|
||||
const metricsBox = easedProgress(frame, 12, 22);
|
||||
const achievementsBox = easedProgress(frame, 30, 20);
|
||||
const deadlinesBox = easedProgress(frame, 46, 20);
|
||||
const sidebarBox = easedProgress(frame, 56, 20);
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline frame={frame} beat={dashboardHeadline} titleSize={72} subtitleSize={22} style={{left: 120, top: 92, width: 620}} />
|
||||
<SignalBadge x={120} y={330} progress={docsProgress} label="Documents" value={`${Math.round(18 * docsProgress)}`} icon="documents" />
|
||||
<SignalBadge x={120} y={424} progress={tasksProgress} label="Active Work" value={`${Math.round(28 * tasksProgress)}`} icon="tasks" />
|
||||
<SignalBadge x={120} y={518} progress={productivityProgress} label="Productivity" value={`${Math.round(78 * productivityProgress)}%`} icon="productivity" />
|
||||
<DemoSurface
|
||||
frame={frame}
|
||||
src={routeScreens.dashboard}
|
||||
style={{left: 310, top: 166}}
|
||||
rotateX={{from: 7, to: 2}}
|
||||
rotateY={{from: -8, to: 1.5}}
|
||||
scale={{from: 1.06, to: 1.01}}
|
||||
translateX={{from: 82, to: -24}}
|
||||
translateY={{from: 24, to: -6}}
|
||||
imageScale={{from: 1.05, to: 1.015}}
|
||||
imageX={{from: -16, to: 0}}
|
||||
imageY={{from: -8, to: 0}}
|
||||
>
|
||||
<HighlightBox x={s(308)} y={s(118)} width={s(942)} height={s(244)} progress={metricsBox} label="Metric density" />
|
||||
<HighlightBox x={s(12)} y={s(78)} width={s(252)} height={s(640)} progress={sidebarBox} label="Route breadth" />
|
||||
<HighlightBox x={s(310)} y={s(392)} width={s(458)} height={s(248)} progress={achievementsBox} label="Recent wins" />
|
||||
<HighlightBox x={s(792)} y={s(392)} width={s(458)} height={s(248)} progress={deadlinesBox} label="Upcoming deadlines" align="top-right" />
|
||||
</DemoSurface>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {EndCard} from '../components/EndCard';
|
||||
import {endCard} from '../data/trailerData';
|
||||
|
||||
export const EndCardScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<EndCard frame={frame} title={endCard.title} subtitle={endCard.subtitle} />
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {DemoSurface} from '../components/DemoSurface';
|
||||
import {HighlightBox} from '../components/HighlightBox';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {SignalBadge} from '../components/SignalBadge';
|
||||
import {routeScreens, taskWorkflowStats, tasksHeadline} from '../data/trailerData';
|
||||
import {easedProgress} from '../lib/motion';
|
||||
|
||||
const SCALE = 1520 / 1280;
|
||||
const s = (value: number) => value * SCALE;
|
||||
|
||||
export const MetricsActivationScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const totalProgress = easedProgress(frame, 8, 28);
|
||||
const activeProgress = easedProgress(frame, 16, 28);
|
||||
const completedProgress = easedProgress(frame, 24, 28);
|
||||
const summaryBox = easedProgress(frame, 12, 18);
|
||||
const filterBox = easedProgress(frame, 28, 18);
|
||||
const firstTaskBox = easedProgress(frame, 46, 18);
|
||||
const secondTaskBox = easedProgress(frame, 64, 18);
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline frame={frame} beat={tasksHeadline} titleSize={68} subtitleSize={22} style={{left: 120, top: 92, width: 660}} />
|
||||
<SignalBadge x={1160} y={112} progress={totalProgress} label={taskWorkflowStats[0].label} value={`${Math.round(taskWorkflowStats[0].value * totalProgress)}`} icon="tasks" />
|
||||
<SignalBadge x={1372} y={112} progress={activeProgress} label={taskWorkflowStats[1].label} value={`${Math.round(taskWorkflowStats[1].value * activeProgress)}`} icon="productivity" />
|
||||
<SignalBadge x={1584} y={112} progress={completedProgress} label={taskWorkflowStats[2].label} value={`${Math.round(taskWorkflowStats[2].value * completedProgress)}`} icon="analytics" />
|
||||
<DemoSurface
|
||||
frame={frame}
|
||||
src={routeScreens.tasks}
|
||||
style={{left: 260, top: 186}}
|
||||
rotateX={{from: 5, to: 1.5}}
|
||||
rotateY={{from: -5, to: 2}}
|
||||
scale={{from: 1.04, to: 1.005}}
|
||||
translateX={{from: 48, to: -18}}
|
||||
translateY={{from: 18, to: -6}}
|
||||
imageScale={{from: 1.035, to: 1.01}}
|
||||
imageX={{from: -8, to: 0}}
|
||||
>
|
||||
<HighlightBox x={s(310)} y={s(170)} width={s(938)} height={s(82)} progress={summaryBox} label="Task totals" />
|
||||
<HighlightBox x={s(310)} y={s(278)} width={s(938)} height={s(104)} progress={filterBox} label="Search and filters" />
|
||||
<HighlightBox x={s(310)} y={s(406)} width={s(938)} height={s(132)} progress={firstTaskBox} label="High-priority task" />
|
||||
<HighlightBox x={s(310)} y={s(558)} width={s(938)} height={s(132)} progress={secondTaskBox} label="Responsive fix" />
|
||||
</DemoSurface>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import {interpolate, useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {IconBadge} from '../components/IconBadge';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {RoutePreviewCard} from '../components/RoutePreviewCard';
|
||||
import {montageHeadline, montageModules, routePreviews} from '../data/trailerData';
|
||||
import {springIn} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
export const MontageScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const centerGlow = springIn(frame, 30, 8, 22);
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline
|
||||
frame={frame}
|
||||
beat={montageHeadline}
|
||||
titleSize={70}
|
||||
subtitleSize={22}
|
||||
maxWidth={1120}
|
||||
style={{left: 300, top: 84, width: 1320, alignItems: 'center'}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 430,
|
||||
top: 232,
|
||||
width: 1060,
|
||||
height: 560,
|
||||
borderRadius: 999,
|
||||
background: 'radial-gradient(circle, rgba(57,185,255,0.2) 0%, rgba(57,185,255,0.05) 38%, rgba(57,185,255,0) 72%)',
|
||||
opacity: centerGlow,
|
||||
transform: `scale(${interpolate(centerGlow, [0, 1], [0.9, 1.08])})`
|
||||
}}
|
||||
/>
|
||||
<RoutePreviewCard beat={routePreviews[0]} progress={springIn(frame, 30, 10, 22)} x={520} y={244} width={880} height={500} emphasis />
|
||||
<RoutePreviewCard beat={routePreviews[1]} progress={springIn(frame, 30, 18, 22)} x={140} y={300} width={330} height={222} />
|
||||
<RoutePreviewCard beat={routePreviews[3]} progress={springIn(frame, 30, 24, 22)} x={1450} y={300} width={330} height={222} />
|
||||
<RoutePreviewCard beat={routePreviews[2]} progress={springIn(frame, 30, 30, 22)} x={220} y={610} width={360} height={236} />
|
||||
<RoutePreviewCard beat={routePreviews[4]} progress={springIn(frame, 30, 36, 22)} x={1340} y={610} width={360} height={236} />
|
||||
<div style={{position: 'absolute', left: 380, top: 866, width: 1160, display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 16}}>
|
||||
{montageModules.map((module, index) => {
|
||||
const progress = springIn(frame, 30, 44 + index * 4, 20);
|
||||
const floatingY = Math.sin((frame + index * 5) / 12) * 3;
|
||||
return (
|
||||
<div
|
||||
key={module.label}
|
||||
style={{
|
||||
height: 62,
|
||||
padding: '0 18px',
|
||||
borderRadius: 18,
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(8, 12, 18, 0.82)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
opacity: progress,
|
||||
transform: `translate3d(0, ${interpolate(progress, [0, 1], [14 - floatingY, floatingY])}px, 0) scale(${interpolate(progress, [0, 1], [0.94, 1])})`,
|
||||
boxShadow: '0 18px 44px rgba(0, 0, 0, 0.34)'
|
||||
}}
|
||||
>
|
||||
<IconBadge icon={module.icon} size={16} />
|
||||
<div style={{fontSize: 18, fontWeight: 700, letterSpacing: '-0.03em', color: trackeepTheme.colors.text}}>{module.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {DemoSurface} from '../components/DemoSurface';
|
||||
import {HighlightBox} from '../components/HighlightBox';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {SignalBadge} from '../components/SignalBadge';
|
||||
import {routeScreens, searchHeadline} from '../data/trailerData';
|
||||
import {easedProgress} from '../lib/motion';
|
||||
|
||||
const SCALE = 1520 / 1280;
|
||||
const s = (value: number) => value * SCALE;
|
||||
|
||||
export const SearchAiScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const queryProgress = easedProgress(frame, 8, 24);
|
||||
const latencyProgress = easedProgress(frame, 18, 24);
|
||||
const matchesProgress = easedProgress(frame, 28, 24);
|
||||
const inputBox = easedProgress(frame, 14, 18);
|
||||
const firstResult = easedProgress(frame, 34, 18);
|
||||
const secondResult = easedProgress(frame, 54, 18);
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline frame={frame} beat={searchHeadline} titleSize={68} subtitleSize={22} style={{left: 120, top: 92, width: 660}} />
|
||||
<SignalBadge x={1260} y={112} progress={queryProgress} label="Query" value="api" icon="search" />
|
||||
<SignalBadge x={1472} y={112} progress={latencyProgress} label="Latency" value="37ms" icon="analytics" />
|
||||
<SignalBadge x={1684} y={112} progress={matchesProgress} label="Matches" value="2" icon="tasks" />
|
||||
<DemoSurface
|
||||
frame={frame}
|
||||
src={routeScreens.search}
|
||||
style={{left: 260, top: 186}}
|
||||
rotateX={{from: 4.5, to: 1.25}}
|
||||
rotateY={{from: -4.5, to: 2}}
|
||||
scale={{from: 1.04, to: 1.005}}
|
||||
translateX={{from: 38, to: -10}}
|
||||
translateY={{from: 18, to: -4}}
|
||||
imageScale={{from: 1.03, to: 1.01}}
|
||||
>
|
||||
<HighlightBox x={s(304)} y={s(255)} width={s(952)} height={s(48)} progress={inputBox} label="Cross-content query" />
|
||||
<HighlightBox x={s(304)} y={s(364)} width={s(952)} height={s(193)} progress={firstResult} label="Task result" />
|
||||
<HighlightBox x={s(304)} y={s(574)} width={s(952)} height={s(144)} progress={secondResult} label="Note result" />
|
||||
</DemoSurface>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import {interpolate, useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../components/Backdrop';
|
||||
import {DemoSurface} from '../components/DemoSurface';
|
||||
import {HighlightBox} from '../components/HighlightBox';
|
||||
import {KineticHeadline} from '../components/KineticHeadline';
|
||||
import {SignalBadge} from '../components/SignalBadge';
|
||||
import {routeScreens, timeHeadline, timeOverviewStats} from '../data/trailerData';
|
||||
import {formatMetricValue, formatStopwatch} from '../lib/format';
|
||||
import {easedProgress} from '../lib/motion';
|
||||
import {trackeepTheme} from '../theme/trackeep';
|
||||
|
||||
const SCALE = 1520 / 1280;
|
||||
const s = (value: number) => value * SCALE;
|
||||
|
||||
export const TimeTrackingScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const timerProgress = easedProgress(frame, 8, 40);
|
||||
const entriesProgress = easedProgress(frame, 16, 28);
|
||||
const billableProgress = easedProgress(frame, 24, 30);
|
||||
const timerBox = easedProgress(frame, 10, 18);
|
||||
const overviewBox = easedProgress(frame, 30, 18);
|
||||
const entriesBox = easedProgress(frame, 50, 18);
|
||||
const buttonProgress = easedProgress(frame, 24, 14);
|
||||
const pulse = 1 + Math.sin(frame / 4) * 0.03;
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline frame={frame} beat={timeHeadline} titleSize={66} subtitleSize={22} style={{left: 120, top: 92, width: 650}} />
|
||||
<SignalBadge x={1100} y={112} progress={timerProgress} label="Tracked Today" value={formatStopwatch(Math.round(315 * timerProgress))} icon="time" />
|
||||
<SignalBadge x={1364} y={112} progress={entriesProgress} label="Entries" value={`${Math.round(timeOverviewStats[1].target * entriesProgress)}`} icon="analytics" />
|
||||
<SignalBadge
|
||||
x={1570}
|
||||
y={112}
|
||||
progress={billableProgress}
|
||||
label="Billable"
|
||||
value={formatMetricValue(timeOverviewStats[2], timeOverviewStats[2].target * billableProgress)}
|
||||
icon="productivity"
|
||||
/>
|
||||
<DemoSurface
|
||||
frame={frame}
|
||||
src={routeScreens.timeTracking}
|
||||
style={{left: 260, top: 188}}
|
||||
rotateX={{from: 4.5, to: 1.5}}
|
||||
rotateY={{from: -4, to: 2}}
|
||||
scale={{from: 1.035, to: 1.005}}
|
||||
translateX={{from: 42, to: -12}}
|
||||
translateY={{from: 20, to: -6}}
|
||||
imageScale={{from: 1.03, to: 1.01}}
|
||||
>
|
||||
<HighlightBox x={s(311)} y={s(120)} width={s(456)} height={s(412)} progress={timerBox} label="Quick capture" />
|
||||
<HighlightBox x={s(792)} y={s(120)} width={s(456)} height={s(412)} progress={overviewBox} label="Today's overview" align="top-right" />
|
||||
<HighlightBox x={s(312)} y={s(558)} width={s(936)} height={s(154)} progress={entriesBox} label="Billable entry" />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: s(337),
|
||||
top: s(362),
|
||||
width: s(406),
|
||||
height: s(36),
|
||||
borderRadius: 10,
|
||||
opacity: buttonProgress,
|
||||
transform: `scale(${pulse})`,
|
||||
boxShadow: `0 0 0 ${interpolate(buttonProgress, [0, 1], [0, 10])}px rgba(57,185,255,0.12), 0 0 46px ${trackeepTheme.colors.primarySoft}`,
|
||||
border: `1px solid rgba(57, 185, 255, 0.65)`
|
||||
}}
|
||||
/>
|
||||
</DemoSurface>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {AppChrome} from '../../components/AppChrome';
|
||||
import {MetricTile} from '../../components/MetricTile';
|
||||
import {ProgressModule} from '../../components/ProgressModule';
|
||||
import {analyticsHeadline, analyticsSummary, habitBeat, learningBeat} from '../../data/trailerData';
|
||||
import {formatAnalyticsHours, formatMetricValue} from '../../lib/format';
|
||||
import {easedProgress} from '../../lib/motion';
|
||||
import {PortraitSceneShell} from './PortraitSceneShell';
|
||||
|
||||
export const PortraitAnalyticsScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<PortraitSceneShell frame={frame} beat={analyticsHeadline}>
|
||||
<div style={{position: 'absolute', left: 90, top: 470}}>
|
||||
<AppChrome frame={frame} activeNav="Stats" sidebarItems={[]} showSidebar={false} width={900} height={980}>
|
||||
<div style={{display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 18, marginBottom: 18}}>
|
||||
{analyticsSummary.map((metric, index) => {
|
||||
const progress = easedProgress(frame, 16 + index * 7, 56);
|
||||
const value = metric.label === 'Hours Tracked'
|
||||
? formatAnalyticsHours(metric.target * progress)
|
||||
: formatMetricValue(metric, metric.target * progress);
|
||||
|
||||
return <MetricTile key={metric.label} frame={frame} delay={12 + index * 5} label={metric.label} value={value} icon={metric.icon} compact />;
|
||||
})}
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 18}}>
|
||||
<ProgressModule frame={frame} delay={54} beat={learningBeat} />
|
||||
<ProgressModule frame={frame} delay={66} beat={habitBeat} />
|
||||
</div>
|
||||
</AppChrome>
|
||||
</div>
|
||||
</PortraitSceneShell>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import trackeepLogo from '../../../../frontend/public/trackeep.svg';
|
||||
import {Img, interpolate, useCurrentFrame} from 'remotion';
|
||||
import {Backdrop} from '../../components/Backdrop';
|
||||
import {trackeepTheme} from '../../theme/trackeep';
|
||||
|
||||
export const PortraitColdOpenScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const logoScale = interpolate(frame, [0, 28], [0.85, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
const lineWidth = interpolate(frame, [18, 60], [0, 240], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<div style={{position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 28}}>
|
||||
<div
|
||||
style={{
|
||||
width: 136,
|
||||
height: 136,
|
||||
borderRadius: 36,
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(57,185,255,0.08)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transform: `scale(${logoScale})`
|
||||
}}
|
||||
>
|
||||
<Img src={trackeepLogo} style={{width: 72, height: 72}} />
|
||||
</div>
|
||||
<div style={{fontSize: 88, fontWeight: 700, letterSpacing: '-0.08em', color: trackeepTheme.colors.text}}>Trackeep</div>
|
||||
<div style={{fontSize: 70, fontWeight: 700, lineHeight: 0.94, letterSpacing: '-0.06em', color: trackeepTheme.colors.text, textAlign: 'center', maxWidth: 860}}>
|
||||
Track everything that matters.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: lineWidth,
|
||||
height: 5,
|
||||
borderRadius: 999,
|
||||
background: `linear-gradient(90deg, ${trackeepTheme.colors.primary} 0%, rgba(57,185,255,0) 100%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {AppChrome} from '../../components/AppChrome';
|
||||
import {MetricTile} from '../../components/MetricTile';
|
||||
import {dashboardHeadline, dashboardHeroMetrics, dashboardSecondaryMetrics} from '../../data/trailerData';
|
||||
import {PortraitSceneShell} from './PortraitSceneShell';
|
||||
|
||||
export const PortraitDashboardScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<PortraitSceneShell frame={frame} beat={dashboardHeadline}>
|
||||
<div style={{position: 'absolute', left: 90, top: 470}}>
|
||||
<AppChrome frame={frame} activeNav="Home" sidebarItems={[]} showSidebar={false} width={900} height={980}>
|
||||
<div style={{display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 18, marginBottom: 18}}>
|
||||
{dashboardHeroMetrics.map((metric, index) => (
|
||||
<MetricTile key={metric.label} frame={frame} delay={16 + index * 5} label={metric.label} value={String(metric.target)} icon={metric.icon} compact />
|
||||
))}
|
||||
</div>
|
||||
<div style={{display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 18}}>
|
||||
{dashboardSecondaryMetrics.map((metric, index) => (
|
||||
<MetricTile
|
||||
key={metric.label}
|
||||
frame={frame}
|
||||
delay={38 + index * 5}
|
||||
label={metric.label}
|
||||
value={metric.decimals ? metric.target.toFixed(metric.decimals) + (metric.suffix ?? '') : `${metric.target}${metric.suffix ?? ''}`}
|
||||
icon={metric.icon}
|
||||
compact
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AppChrome>
|
||||
</div>
|
||||
</PortraitSceneShell>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {AppChrome} from '../../components/AppChrome';
|
||||
import {ChartBars} from '../../components/ChartBars';
|
||||
import {MetricTile} from '../../components/MetricTile';
|
||||
import {ProgressModule} from '../../components/ProgressModule';
|
||||
import {activationHeadline, activationMetrics, storageBeat, weeklyActivity} from '../../data/trailerData';
|
||||
import {formatMetricValue} from '../../lib/format';
|
||||
import {easedProgress} from '../../lib/motion';
|
||||
import {PortraitSceneShell} from './PortraitSceneShell';
|
||||
|
||||
export const PortraitMetricsScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<PortraitSceneShell frame={frame} beat={activationHeadline}>
|
||||
<div style={{position: 'absolute', left: 90, top: 446}}>
|
||||
<AppChrome
|
||||
frame={frame}
|
||||
activeNav="Home"
|
||||
sidebarItems={[]}
|
||||
showSidebar={false}
|
||||
width={900}
|
||||
height={1120}
|
||||
contentStyle={{padding: '24px 24px 26px'}}
|
||||
>
|
||||
<div style={{display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 16, marginBottom: 16}}>
|
||||
{activationMetrics.slice(0, 6).map((metric, index) => {
|
||||
const progress = easedProgress(frame, 16 + index * 5, 42);
|
||||
return (
|
||||
<MetricTile
|
||||
key={metric.label}
|
||||
frame={frame}
|
||||
delay={10 + index * 4}
|
||||
label={metric.label}
|
||||
value={formatMetricValue(metric, metric.target * progress)}
|
||||
icon={metric.icon}
|
||||
compact
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
|
||||
<ChartBars frame={frame} delay={54} values={weeklyActivity} />
|
||||
<ProgressModule frame={frame} delay={62} beat={storageBeat} />
|
||||
</div>
|
||||
</AppChrome>
|
||||
</div>
|
||||
</PortraitSceneShell>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import {interpolate, useCurrentFrame} from 'remotion';
|
||||
import {IconBadge} from '../../components/IconBadge';
|
||||
import {KineticHeadline} from '../../components/KineticHeadline';
|
||||
import {RoutePreviewCard} from '../../components/RoutePreviewCard';
|
||||
import {montageHeadline, montageModules, routePreviews} from '../../data/trailerData';
|
||||
import {springIn} from '../../lib/motion';
|
||||
import {trackeepTheme} from '../../theme/trackeep';
|
||||
import {Backdrop} from '../../components/Backdrop';
|
||||
|
||||
export const PortraitMontageScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const centerGlow = springIn(frame, 30, 8, 20);
|
||||
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline
|
||||
frame={frame}
|
||||
beat={{...montageHeadline, align: 'center'}}
|
||||
titleSize={66}
|
||||
subtitleSize={24}
|
||||
kickerSize={15}
|
||||
maxWidth={900}
|
||||
style={{left: 90, top: 112, width: 900, alignItems: 'center'}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 110,
|
||||
top: 420,
|
||||
width: 860,
|
||||
height: 540,
|
||||
borderRadius: 999,
|
||||
background: 'radial-gradient(circle, rgba(57,185,255,0.2) 0%, rgba(57,185,255,0.04) 42%, rgba(57,185,255,0) 75%)',
|
||||
opacity: centerGlow,
|
||||
transform: `scale(${interpolate(centerGlow, [0, 1], [0.9, 1.08])})`
|
||||
}}
|
||||
/>
|
||||
<RoutePreviewCard beat={routePreviews[0]} progress={springIn(frame, 30, 10, 20)} x={160} y={418} width={760} height={476} emphasis />
|
||||
<RoutePreviewCard beat={routePreviews[1]} progress={springIn(frame, 30, 18, 20)} x={120} y={952} width={390} height={248} />
|
||||
<RoutePreviewCard beat={routePreviews[4]} progress={springIn(frame, 30, 24, 20)} x={570} y={952} width={390} height={248} />
|
||||
<RoutePreviewCard beat={routePreviews[3]} progress={springIn(frame, 30, 30, 20)} x={160} y={1260} width={760} height={304} />
|
||||
<div style={{position: 'absolute', left: 120, top: 1610, width: 840, display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 16}}>
|
||||
{montageModules.map((module, index) => {
|
||||
const entry = springIn(frame, 30, 38 + index * 4, 18);
|
||||
const floatingY = Math.sin((frame + index * 5) / 12) * 2;
|
||||
return (
|
||||
<div
|
||||
key={module.label}
|
||||
style={{
|
||||
height: 106,
|
||||
borderRadius: 22,
|
||||
border: `1px solid ${trackeepTheme.colors.borderStrong}`,
|
||||
background: 'rgba(9,11,16,0.76)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
padding: '0 22px',
|
||||
opacity: entry,
|
||||
transform: `translate3d(0, ${interpolate(entry, [0, 1], [20 - floatingY, floatingY])}px, 0) scale(${interpolate(entry, [0, 1], [0.94, 1])})`,
|
||||
boxShadow: '0 20px 54px rgba(0, 0, 0, 0.38)'
|
||||
}}
|
||||
>
|
||||
<IconBadge icon={module.icon} size={20} />
|
||||
<div style={{fontSize: 30, fontWeight: 700, color: trackeepTheme.colors.text, letterSpacing: '-0.05em'}}>{module.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {Backdrop} from '../../components/Backdrop';
|
||||
import {KineticHeadline} from '../../components/KineticHeadline';
|
||||
import type {HeadlineBeat} from '../../data/types';
|
||||
|
||||
interface PortraitSceneShellProps {
|
||||
frame: number;
|
||||
beat: HeadlineBeat;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const PortraitSceneShell = ({frame, beat, children}: PortraitSceneShellProps) => {
|
||||
return (
|
||||
<Backdrop>
|
||||
<KineticHeadline
|
||||
frame={frame}
|
||||
beat={{...beat, align: 'center'}}
|
||||
titleSize={72}
|
||||
subtitleSize={24}
|
||||
kickerSize={15}
|
||||
maxWidth={860}
|
||||
style={{left: 110, top: 112, width: 860, alignItems: 'center'}}
|
||||
/>
|
||||
{children}
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {AppChrome} from '../../components/AppChrome';
|
||||
import {TypingMessage} from '../../components/TypingMessage';
|
||||
import {searchBeat, searchHeadline} from '../../data/trailerData';
|
||||
import {easedProgress} from '../../lib/motion';
|
||||
import {trackeepTheme} from '../../theme/trackeep';
|
||||
import {PortraitSceneShell} from './PortraitSceneShell';
|
||||
|
||||
export const PortraitSearchAiScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const resultProgress = easedProgress(frame, 44, 54);
|
||||
|
||||
return (
|
||||
<PortraitSceneShell frame={frame} beat={searchHeadline}>
|
||||
<div style={{position: 'absolute', left: 90, top: 470}}>
|
||||
<AppChrome
|
||||
frame={frame}
|
||||
activeNav="AI Assistant"
|
||||
sidebarItems={[]}
|
||||
showSidebar={false}
|
||||
searchText={searchBeat.query}
|
||||
searchDelay={18}
|
||||
width={900}
|
||||
height={980}
|
||||
>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 18}}>
|
||||
<div style={{borderRadius: 22, border: `1px solid ${trackeepTheme.colors.border}`, background: 'rgba(255,255,255,0.02)', padding: '22px 24px'}}>
|
||||
<div style={{fontSize: 28, fontWeight: 700, color: trackeepTheme.colors.text, marginBottom: 14}}>Priority results</div>
|
||||
{['API Documentation', 'Review pull requests', 'Fix responsive issues'].map((item, index) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
padding: '16px 18px',
|
||||
marginTop: index === 0 ? 0 : 12,
|
||||
opacity: resultProgress
|
||||
}}
|
||||
>
|
||||
<div style={{fontSize: 24, fontWeight: 600, color: trackeepTheme.colors.text}}>{item}</div>
|
||||
<div style={{fontSize: 18, color: trackeepTheme.colors.textMuted, marginTop: 6}}>{['Closest deadline', 'Needs review', 'Shipping blocker'][index]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{borderRadius: 22, border: `1px solid ${trackeepTheme.colors.border}`, background: 'rgba(255,255,255,0.02)', padding: '22px 24px'}}>
|
||||
<div style={{fontSize: 28, fontWeight: 700, color: trackeepTheme.colors.text, marginBottom: 8}}>AI Assistant</div>
|
||||
<div style={{fontSize: 18, color: trackeepTheme.colors.textMuted, marginBottom: 18}}>Your intelligent workspace companion</div>
|
||||
<TypingMessage frame={frame} delay={72} text={searchBeat.reply} />
|
||||
</div>
|
||||
</div>
|
||||
</AppChrome>
|
||||
</div>
|
||||
</PortraitSceneShell>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import {useCurrentFrame} from 'remotion';
|
||||
import {AppChrome} from '../../components/AppChrome';
|
||||
import {IconBadge} from '../../components/IconBadge';
|
||||
import {timeHeadline, timeOverviewStats} from '../../data/trailerData';
|
||||
import {formatMetricValue, formatStopwatch} from '../../lib/format';
|
||||
import {easedProgress} from '../../lib/motion';
|
||||
import {trackeepTheme} from '../../theme/trackeep';
|
||||
import {PortraitSceneShell} from './PortraitSceneShell';
|
||||
|
||||
const panelStyle = {
|
||||
borderRadius: 22,
|
||||
border: `1px solid ${trackeepTheme.colors.border}`,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
padding: '22px 24px'
|
||||
};
|
||||
|
||||
export const PortraitTimeTrackingScene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const timerProgress = easedProgress(frame, 12, 96);
|
||||
const timerSeconds = Math.round(315 * timerProgress);
|
||||
const billableProgress = easedProgress(frame, 34, 72);
|
||||
|
||||
return (
|
||||
<PortraitSceneShell frame={frame} beat={timeHeadline}>
|
||||
<div style={{position: 'absolute', left: 90, top: 470}}>
|
||||
<AppChrome frame={frame} activeNav="Time Tracking" sidebarItems={[]} showSidebar={false} width={900} height={1050}>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: 18}}>
|
||||
<div style={{...panelStyle, minHeight: 360, display: 'flex', flexDirection: 'column', gap: 18}}>
|
||||
<div style={{fontSize: 110, fontWeight: 700, letterSpacing: '-0.07em', color: trackeepTheme.colors.text, textAlign: 'center'}}>{formatStopwatch(timerSeconds)}</div>
|
||||
<div style={{textAlign: 'center', fontSize: 30, color: trackeepTheme.colors.textMuted}}>Stopped</div>
|
||||
<div style={{height: 60, borderRadius: 12, border: `1px solid ${trackeepTheme.colors.border}`, display: 'flex', alignItems: 'center', padding: '0 16px', color: trackeepTheme.colors.textMuted, fontSize: 22}}>What are you working on? (optional)</div>
|
||||
<div style={{height: 60, borderRadius: 12, border: `1px solid ${trackeepTheme.colors.border}`, display: 'flex', alignItems: 'center', padding: '0 16px', color: trackeepTheme.colors.textMuted, fontSize: 22}}>Add tags...</div>
|
||||
<div style={{height: 60, borderRadius: 12, background: trackeepTheme.colors.primary, color: '#03131d', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28, fontWeight: 700}}>Start</div>
|
||||
</div>
|
||||
<div style={{...panelStyle}}>
|
||||
<div style={{fontSize: 28, fontWeight: 700, color: trackeepTheme.colors.text, marginBottom: 18}}>Today's Overview</div>
|
||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18}}>
|
||||
{timeOverviewStats.map((stat, index) => {
|
||||
const progress = index === 2 ? billableProgress : easedProgress(frame, 22 + index * 8, 56);
|
||||
const animatedValue = stat.target * progress;
|
||||
const value = stat.label === 'Total Time Today'
|
||||
? formatStopwatch(Math.round(animatedValue))
|
||||
: formatMetricValue(stat, animatedValue);
|
||||
return (
|
||||
<div key={stat.label} style={{display: 'flex', alignItems: 'center', gap: 12}}>
|
||||
<IconBadge icon={stat.icon} />
|
||||
<div>
|
||||
<div style={{fontSize: 34, fontWeight: 700, color: trackeepTheme.colors.text, letterSpacing: '-0.05em'}}>{value}</div>
|
||||
<div style={{fontSize: 18, color: trackeepTheme.colors.textMuted}}>{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppChrome>
|
||||
</div>
|
||||
</PortraitSceneShell>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
@import "@fontsource/inter/400.css";
|
||||
@import "@fontsource/inter/500.css";
|
||||
@import "@fontsource/inter/600.css";
|
||||
@import "@fontsource/inter/700.css";
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #050608;
|
||||
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export const trackeepTheme = {
|
||||
colors: {
|
||||
background: '#141415',
|
||||
backgroundAlt: '#1a1a1a',
|
||||
backgroundDeep: '#0b0d11',
|
||||
surface: '#17191d',
|
||||
surfaceRaised: '#202228',
|
||||
surfaceSoft: '#121418',
|
||||
border: '#262626',
|
||||
borderStrong: '#2d3139',
|
||||
text: '#fafafa',
|
||||
textMuted: '#a3a3a3',
|
||||
primary: '#39b9ff',
|
||||
primarySoft: 'rgba(57, 185, 255, 0.16)',
|
||||
primaryGlow: 'rgba(57, 185, 255, 0.28)',
|
||||
success: '#61d88f',
|
||||
warning: '#f8b84e',
|
||||
danger: '#ff6868'
|
||||
},
|
||||
radius: {
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 18,
|
||||
pill: 999
|
||||
},
|
||||
shadow: {
|
||||
soft: '0 18px 60px rgba(0, 0, 0, 0.34)',
|
||||
card: '0 12px 32px rgba(0, 0, 0, 0.2)'
|
||||
},
|
||||
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
||||
} as const;
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user