log-stream
Paced runtime logs with levels and bounded scrolling.
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>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/log-streamnpx odori add @odori/log-streambunx odori add @odori/log-streamSource is copied into videos/components/log-stream/ 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/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",
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"},
],
},
},
],
});
Use it in 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>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
- 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 | — | — |