workflow-steps
Sequential process with one active focal point.
import {Scene, Video} from "odori";
import {WorkflowSteps} from "../components/workflow-steps/workflow-steps";
<Video>
<Scene id="workflow-steps" duration="6s">
<WorkflowSteps
steps={[
"Author",
"Preview",
"Test",
"Export"
]}
headline="From file to finished cut"
hold={40}
/>
</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/workflow-stepsnpx odori add @odori/workflow-stepsbunx odori add @odori/workflow-stepsSource is copied into videos/components/workflow-steps/ 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/workflow-steps/workflow-steps.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale, useVideo} from "odori";
export type WorkflowStepsProps = {
steps: string[];
headline?: string;
/** Frames each step holds the focus. */
hold?: number;
};
/**
* A sequential process with exactly one active step. Completed steps stay on
* screen at reduced weight, so the viewer keeps the whole path in view while
* the focus advances.
*/
export const WorkflowSteps = ({steps, headline, hold = 40}: WorkflowStepsProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {width, height} = useVideo();
const stacked = height >= width;
const active = Math.min(steps.length - 1, Math.floor(Math.max(0, frame - 14) / hold));
return (
<Fill style={{gap: 46 * scale, justifyContent: "center", padding: `${110 * scale}px ${120 * scale}px`}}>
{headline ? (
<div style={{fontSize: 58 * scale, fontWeight: 560, letterSpacing: "-0.04em", opacity: interpolate(frame, [0, 16], [0, 1])}}>
{headline}
</div>
) : null}
<div style={{alignItems: "stretch", display: "flex", flexDirection: stacked ? "column" : "row", gap: 20 * scale}}>
{steps.map((step, index) => {
const enter = interpolate(frame, [10 + index * 6, 10 + index * 6 + 18], [0, 1], {easing: Easing.standard});
const isActive = index === active;
const done = index < active;
return (
<div
key={step}
style={{
alignItems: "center",
background: isActive ? `color-mix(in srgb, ${brand.colors.accent} 14%, ${brand.colors.surface})` : brand.colors.surface,
border: `1px solid ${isActive ? brand.colors.accent : brand.colors.border}`,
borderRadius: 22 * scale,
display: "flex",
flex: 1,
gap: 18 * scale,
opacity: enter * (done ? 0.6 : 1),
padding: `${26 * scale}px ${28 * scale}px`,
transform: `translateY(${(1 - enter) * 16 * scale}px)`,
}}
>
<span
style={{
alignItems: "center",
background: isActive ? brand.colors.accent : "transparent",
border: `${2 * scale}px solid ${isActive ? brand.colors.accent : brand.colors.border}`,
borderRadius: 999,
color: isActive ? brand.colors.background : brand.colors.muted,
display: "flex",
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
height: 46 * scale,
justifyContent: "center",
width: 46 * scale,
}}
>
{index + 1}
</span>
<span style={{fontSize: 34 * scale, letterSpacing: "-0.02em"}}>{step}</span>
</div>
);
})}
</div>
</Fill>
);
};
videos/components/workflow-steps/workflow-steps.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {WorkflowSteps} from "./workflow-steps";
export default defineComponentPreview({
title: "Workflow steps",
category: "Narrative",
description: "Sequential process with one active focal point.",
component: WorkflowSteps,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
headline: {type: "text", defaultValue: "From file to finished cut"},
hold: {type: "number", defaultValue: 40, min: 16, max: 90},
},
examples: [
{name: "Pipeline", props: {steps: ["Author", "Preview", "Test", "Export"]}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {WorkflowSteps} from "../components/workflow-steps/workflow-steps";
<Video>
<Scene id="workflow-steps" duration="6s">
<WorkflowSteps
steps={[
"Author",
"Preview",
"Test",
"Export"
]}
headline="From file to finished cut"
hold={40}
/>
</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
- Narrative
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 180 frames · 6s
- Minimum duration
- 90 frames · 3s
- Entrance
- 30 frames
- Exit
- 0 frames
- Reduced motion
- every step shows without a moving focus
- Requires
- sans font, mono font
- Content limits
- steps ≤ 5, stepLength ≤ 26
Props
| Prop | Type | Default | Required |
|---|---|---|---|
steps | string[] | — | Yes |
headline | string | — | — |
holdFrames each step holds the focus. | number | 40 | — |