code-walkthrough
Progressive code reveal with authored focal regions.
import {Scene, Video} from "odori";
import {CodeWalkthrough} from "../components/code-walkthrough/code-walkthrough";
<Video>
<Scene id="code-walkthrough" duration="8s">
<CodeWalkthrough
code={`export const metadata = defineVideoMetadata({
id: "launch",
duration: "8s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene duration="5s">
<TitleReveal title="Ship the story." />
</Scene>
</Video>
);
}`}
steps={[
{
from: 1,
to: 4,
note: "Static metadata, so discovery never runs your component."
},
{
from: 6,
to: 13,
note: "Ordinary JSX for the timeline."
},
{
from: 8,
to: 10,
note: "Scenes are ordered, not numbered."
}
]}
title="videos/launch/video.tsx"
hold={60}
/>
</Scene>
</Video>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/code-walkthroughnpx odori add @odori/code-walkthroughbunx odori add @odori/code-walkthroughSource is copied into videos/components/code-walkthrough/ and belongs to your repository. odori diff compares it with upstream later; odori update applies the change you accept. It has no registry dependencies.
Copy and paste the following code into your project.
videos/components/code-walkthrough/code-walkthrough.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type WalkthroughStep = {
/** Inclusive line range to focus, one-based. */
from: number;
to: number;
note?: string;
};
export type CodeWalkthroughProps = {
code: string;
steps: WalkthroughStep[];
title?: string;
/** Frames each step holds before the next takes focus. */
hold?: number;
};
/**
* Source read in authored order. Every line stays on screen the whole time, so
* the reader keeps their place; focus moves by dimming everything the current
* step is not about.
*/
export const CodeWalkthrough = ({code, steps, title = "videos/launch/video.tsx", hold = 60}: CodeWalkthroughProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const lines = code.split("\n");
const index = Math.min(steps.length - 1, Math.floor(Math.max(0, frame - 12) / hold));
const step = steps[index];
const stepStart = 12 + index * hold;
const focus = interpolate(frame, [stepStart, stepStart + 16], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", justifyContent: "center", padding: 90 * scale}}>
<div
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 26 * scale,
minWidth: 1180 * scale,
overflow: "hidden",
}}
>
<div
style={{
borderBottom: `1px solid ${brand.colors.border}`,
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
padding: `${20 * scale}px ${28 * scale}px`,
}}
>
{title}
</div>
<div style={{padding: `${20 * scale}px 0`}}>
{lines.map((line, lineIndex) => {
const number = lineIndex + 1;
const inFocus = step && number >= step.from && number <= step.to;
const dim = inFocus ? 1 : 1 - 0.68 * focus;
return (
<div
key={`${line}-${lineIndex}`}
style={{
alignItems: "center",
background: inFocus
? `color-mix(in srgb, ${brand.colors.accent} ${focus * 10}%, transparent)`
: "transparent",
display: "flex",
gap: 24 * scale,
opacity: dim,
padding: `${6 * scale}px ${28 * scale}px`,
}}
>
<span style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 24 * scale, width: 44 * scale}}>
{number}
</span>
<span style={{fontFamily: brand.typography.mono, fontSize: 30 * scale, whiteSpace: "pre"}}>{line}</span>
</div>
);
})}
</div>
{step?.note ? (
<div
style={{
borderTop: `1px solid ${brand.colors.border}`,
color: brand.colors.muted,
fontSize: 30 * scale,
opacity: focus,
padding: `${22 * scale}px ${28 * scale}px`,
}}
>
{step.note}
</div>
) : null}
</div>
</Fill>
);
};
videos/components/code-walkthrough/code-walkthrough.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {CodeWalkthrough} from "./code-walkthrough";
export default defineComponentPreview({
title: "Code walkthrough",
category: "Developer proof",
description: "Progressive code reveal with authored focal regions.",
component: CodeWalkthrough,
canvas: {width: 1920, height: 1080, duration: "8s"},
controls: {
title: {type: "text", defaultValue: "videos/launch/video.tsx"},
hold: {type: "number", defaultValue: 60, min: 24, max: 120},
},
examples: [
{
name: "Metadata to timeline",
props: {
code: `export const metadata = defineVideoMetadata({
id: "launch",
duration: "8s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene duration="5s">
<TitleReveal title="Ship the story." />
</Scene>
</Video>
);
}`,
steps: [
{from: 1, to: 4, note: "Static metadata, so discovery never runs your component."},
{from: 6, to: 13, note: "Ordinary JSX for the timeline."},
{from: 8, to: 10, note: "Scenes are ordered, not numbered."},
],
},
},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {CodeWalkthrough} from "../components/code-walkthrough/code-walkthrough";
<Video>
<Scene id="code-walkthrough" duration="8s">
<CodeWalkthrough
code={`export const metadata = defineVideoMetadata({
id: "launch",
duration: "8s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene duration="5s">
<TitleReveal title="Ship the story." />
</Scene>
</Video>
);
}`}
steps={[
{
from: 1,
to: 4,
note: "Static metadata, so discovery never runs your component."
},
{
from: 6,
to: 13,
note: "Ordinary JSX for the timeline."
},
{
from: 8,
to: 10,
note: "Scenes are ordered, not numbered."
}
]}
title="videos/launch/video.tsx"
hold={60}
/>
</Scene>
</Video>Nothing else to install.
The component imports only from odori, so there are no dependencies to add and no import paths to rewrite.
Timing contract
Every registry component declares how it behaves in time.odori test fails a cut that violates these.
- Family
- Developer proof
- Aspect ratios
- 16:9 · 1:1
- Recommended duration
- 240 frames · 8s
- Minimum duration
- 120 frames · 4s
- Entrance
- 28 frames
- Exit
- 0 frames
- Reduced motion
- all steps show without dimming
- Requires
- mono font
- Content limits
- lines ≤ 16, lineLength ≤ 62, steps ≤ 4
Props
| Prop | Type | Default | Required |
|---|---|---|---|
code | string | — | Yes |
steps | WalkthroughStep[] | — | Yes |
title | string | "videos/launch/video.tsx" | — |
holdFrames each step holds before the next takes focus. | number | 60 | — |