Log stream
Paced runtime logs with levels and bounded scrolling.
Customize
import {Scene, Video} from "odori";
import {LogStream} from "../components/log-stream/log-stream";
<Video>
<Scene id="log-stream" duration="6s">
<LogStream
entries={[
{
message: "resolved manifest launch@2f9c",
level: "info"
},
{
message: "sampling 12 representative frames",
level: "info"
},
{
message: "scene proof runs 18f under its contract",
level: "warn"
},
{
message: "rendering 360 frames",
level: "info"
},
{
message: "mixing audio to -14 LUFS",
level: "info"
},
{
message: "wrote out/launch.mp4",
level: "done"
}
]}
title="odori export launch"
window={6}
/>
</Scene>
</Video>Installation
pnpm odori add component log-streamnpx odori add component log-streambunx odori add component log-streamEdit the source in videos/components/log-stream/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/log-stream/log-stream.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type LogEntry = {
message: string;
level?: "info" | "warn" | "error" | "done";
/** Frame the line appears on. Derived from the index when omitted. */
atFrame?: number;
};
export type LogStreamProps = {
entries: LogEntry[];
title?: string;
/** Lines visible at once before the stream scrolls. */
window?: number;
};
/**
* Runtime logs arriving on a schedule, bounded so the panel never grows past
* the frame. Older lines scroll out of the window rather than shrinking the
* type to fit.
*/
export const LogStream = ({entries, title = "odori export launch", window: visible = 6}: LogStreamProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const arrived = entries.filter((entry, index) => frame >= (entry.atFrame ?? 14 + index * 12));
const shown = arrived.slice(Math.max(0, arrived.length - visible));
const color = (level: LogEntry["level"]) =>
level === "error" || level === "warn" ? brand.colors.accent : level === "done" ? brand.colors.foreground : brand.colors.muted;
return (
<Fill style={{alignItems: "center", justifyContent: "center", padding: 90 * scale}}>
<div
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 24 * scale,
minHeight: visible * 56 * scale,
minWidth: 1180 * scale,
overflow: "hidden",
padding: `${24 * scale}px 0`,
}}
>
<div
style={{
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
padding: `0 ${28 * scale}px ${16 * scale}px`,
}}
>
{title}
</div>
{shown.map((entry, index) => {
const at = entry.atFrame ?? 14 + entries.indexOf(entry) * 12;
const enter = interpolate(frame, [at, at + 10], [0, 1], {easing: Easing.standard});
return (
<div
key={`${entry.message}-${index}`}
style={{
alignItems: "center",
color: color(entry.level),
display: "flex",
fontFamily: brand.typography.mono,
fontSize: 28 * scale,
gap: 16 * scale,
opacity: enter,
padding: `${8 * scale}px ${28 * scale}px`,
transform: `translateY(${(1 - enter) * 10 * scale}px)`,
}}
>
<span style={{opacity: 0.55, width: 90 * scale}}>{(entry.level ?? "info").toUpperCase()}</span>
<span>{entry.message}</span>
</div>
);
})}
</div>
</Fill>
);
};
videos/components/log-stream/log-stream.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {LogStream} from "./log-stream";
export default defineComponentPreview({
title: "Log stream",
category: "Developer proof/Terminal",
description: "Paced runtime logs with levels and bounded scrolling.",
component: LogStream,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
title: {type: "text", defaultValue: "odori export launch"},
window: {type: "number", defaultValue: 6, min: 3, max: 10},
},
examples: [
{
name: "Export",
props: {
entries: [
{message: "resolved manifest launch@2f9c", level: "info"},
{message: "sampling 12 representative frames", level: "info"},
{message: "scene proof runs 18f under its contract", level: "warn"},
{message: "rendering 360 frames", level: "info"},
{message: "mixing audio to -14 LUFS", level: "info"},
{message: "wrote out/launch.mp4", level: "done"},
],
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {LogStream} from "../components/log-stream/log-stream";
<Video>
<Scene id="log-stream" duration="6s">
<LogStream
entries={[
{
message: "resolved manifest launch@2f9c",
level: "info"
},
{
message: "sampling 12 representative frames",
level: "info"
},
{
message: "scene proof runs 18f under its contract",
level: "warn"
},
{
message: "rendering 360 frames",
level: "info"
},
{
message: "mixing audio to -14 LUFS",
level: "info"
},
{
message: "wrote out/launch.mp4",
level: "done"
}
]}
title="odori export launch"
window={6}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Developer proof
- Recommended duration
- 180 frames · 6s
- Minimum duration
- 90 frames · 3s
- Entrance
- 14 frames
- Exit
- 0 frames
- Reduced motion
- lines appear without travel
- Requires
- mono font
- Content limits
- entries ≤ 10, messageLength ≤ 56
Props
| Prop | Type | Default | Required |
|---|---|---|---|
entries | LogEntry[] | — | Yes |
title | string | "odori export launch" | — |
windowLines visible at once before the stream scrolls. | number | — | — |
Related components
All componentsProduct launchA framed source excerpt that reveals line by line.Code walkthroughProgressive code reveal with authored focal regions.Dependency graphPackages and modules connected as a readable graph.Diff proofBefore and after source changes with semantic emphasis.File treeProgressive repository navigation with active file state.TerminalA typed command session with deterministic output reveals.