data-table
Sortable rows with an authored selection story.
import {Scene, Video} from "odori";
import {DataTable} from "../components/data-table/data-table";
<Video>
<Scene id="data-table" duration="5s">
<DataTable
columns={[
"Video",
"Format",
"Duration",
"State"
]}
rows={[
[
"launch",
"1920x1080",
"12s",
"queued"
],
[
"launch",
"1080x1920",
"12s",
"rendering"
],
[
"announcement",
"1080x1080",
"8s",
"done"
]
]}
selectedIndex={1}
title="Export jobs"
/>
</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/data-tablenpx odori add @odori/data-tablebunx odori add @odori/data-tableSource is copied into videos/components/data-table/ 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/data-table/data-table.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type DataTableProps = {
columns: string[];
rows: string[][];
/** Row that ends up selected, zero based. */
selectedIndex?: number;
title?: string;
};
/**
* Rows arriving in order, ending on one authored selection. The selected row
* fills rather than recolors its text, so the story survives a brand where
* accent and foreground sit close together.
*/
export const DataTable = ({columns, rows, selectedIndex, title}: DataTableProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const selectAt = 16 + rows.length * 7;
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: 1200 * scale,
overflow: "hidden",
}}
>
{title ? (
<div
style={{
borderBottom: `1px solid ${brand.colors.border}`,
fontSize: 40 * scale,
fontWeight: 560,
padding: `${24 * scale}px ${30 * scale}px`,
}}
>
{title}
</div>
) : null}
<div
style={{
color: brand.colors.muted,
display: "grid",
fontSize: 26 * scale,
gap: 20 * scale,
gridTemplateColumns: `repeat(${columns.length}, 1fr)`,
padding: `${20 * scale}px ${30 * scale}px`,
}}
>
{columns.map((column) => (
<span key={column}>{column}</span>
))}
</div>
{rows.map((row, index) => {
const delay = 14 + index * 7;
const enter = interpolate(frame, [delay, delay + 16], [0, 1], {easing: Easing.standard});
const selected = index === selectedIndex;
const fill = selected ? interpolate(frame, [selectAt, selectAt + 16], [0, 1], {easing: Easing.standard}) : 0;
return (
<div
key={index}
style={{
background: `color-mix(in srgb, ${brand.colors.accent} ${fill * 14}%, transparent)`,
borderTop: `1px solid ${brand.colors.border}`,
display: "grid",
fontSize: 30 * scale,
gap: 20 * scale,
gridTemplateColumns: `repeat(${columns.length}, 1fr)`,
opacity: enter,
padding: `${20 * scale}px ${30 * scale}px`,
transform: `translateY(${(1 - enter) * 10 * scale}px)`,
}}
>
{row.map((cell, cellIndex) => (
<span key={cellIndex} style={{fontFamily: cellIndex === 0 ? brand.typography.sans : brand.typography.mono}}>
{cell}
</span>
))}
</div>
);
})}
</div>
</Fill>
);
};
videos/components/data-table/data-table.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {DataTable} from "./data-table";
export default defineComponentPreview({
title: "Data table",
category: "Data",
description: "Sortable rows with an authored selection story.",
component: DataTable,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {
title: {type: "text", defaultValue: "Export jobs"},
selectedIndex: {type: "number", defaultValue: 1, min: 0, max: 5},
},
examples: [
{
name: "Jobs",
props: {
columns: ["Video", "Format", "Duration", "State"],
rows: [
["launch", "1920x1080", "12s", "queued"],
["launch", "1080x1920", "12s", "rendering"],
["announcement", "1080x1080", "8s", "done"],
],
selectedIndex: 1,
},
},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {DataTable} from "../components/data-table/data-table";
<Video>
<Scene id="data-table" duration="5s">
<DataTable
columns={[
"Video",
"Format",
"Duration",
"State"
]}
rows={[
[
"launch",
"1920x1080",
"12s",
"queued"
],
[
"launch",
"1080x1920",
"12s",
"rendering"
],
[
"announcement",
"1080x1080",
"8s",
"done"
]
]}
selectedIndex={1}
title="Export jobs"
/>
</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
- Data
- Aspect ratios
- 16:9 · 1:1
- Recommended duration
- 150 frames · 5s
- Minimum duration
- 75 frames · 2.5s
- Entrance
- 60 frames
- Exit
- 0 frames
- Reduced motion
- rows and the selection appear at once
- Requires
- sans font, mono font
- Content limits
- rows ≤ 6, columns ≤ 5
Props
| Prop | Type | Default | Required |
|---|---|---|---|
columns | string[] | — | Yes |
rows | string[][] | — | Yes |
selectedIndexRow that ends up selected, zero based. | number | — | — |
title | string | — | — |