/** * Custom-template craft kit — CustomTable. * * Brand-themed data table (the "ticker"/ledger half of the data-viz pair, the * counterpart to CustomChart). Generalized from chronicle/ChronicleTable and * bloomberg/nightfall tables: small-caps header row, zebra rows, tabular figures, * +/- coloring on an optional highlight column, staggered row reveal. * * Reads a plain { headers, rows } table — the same shape the pipeline binds from * Firecrawl-extracted tables — so it stays editable via the standard chart_table * editor in SceneEditModal. */ import React from "react"; import { useCurrentFrame, useVideoConfig } from "remotion"; import { useKit } from "./context"; import { withAlpha } from "./theme"; import { progressAt, easeOutQuint } from "./motion"; export interface CustomTableData { headers?: string[]; rows?: Array>; } export interface CustomTableProps { table?: CustomTableData; /** Column index to color by sign (+green / -red). */ highlightCol?: number; maxRows?: number; maxCols?: number; start?: number; } function parseNumericCell(raw: string): number { const s = String(raw ?? "").trim(); const wrapped = s.match(/^\(([0-9,.]+)\)$/); const signed = wrapped ? `-${wrapped[1]}` : s; return parseFloat(signed.replace(/[^0-9.+\-]/g, "")); } export const CustomTable: React.FC = ({ table, highlightCol = -1, maxRows = 12, maxCols = 6, start = 0, }) => { const frame = useCurrentFrame(); const { height } = useVideoConfig(); const { palette, type, fonts } = useKit(); const headers = (table?.headers ?? []).slice(0, maxCols).map((h) => String(h ?? "")); const rows = (table?.rows ?? []) .slice(0, maxRows) .map((r) => (r ?? []).slice(0, maxCols).map((c) => String(c ?? ""))); const nCols = Math.max(headers.length, rows.reduce((m, r) => Math.max(m, r.length), 0), 1); const cellFs = Math.round(type.body * (nCols >= 5 ? 0.72 : nCols >= 4 ? 0.8 : 0.9)); const headFs = Math.round(cellFs * 0.92); const padV = nCols >= 5 ? 9 : 12; const padH = nCols <= 4 ? 16 : 11; const POS = palette.isDark ? "#5BD08A" : "#1F8A4C"; const NEG = palette.isDark ? "#FF7A6E" : "#C23B2E"; const hlColor = (raw: string): string | undefined => { if (highlightCol < 0) return undefined; const n = parseNumericCell(raw); if (!Number.isFinite(n) || n === 0) return undefined; return n > 0 ? POS : NEG; }; const cellBorder = (ci: number) => ci < nCols - 1 ? `1px solid ${withAlpha(palette.text, 0.1)}` : "none"; if (!rows.length && !headers.length) return null; return (
{headers.length > 0 && (
{headers.map((h, ci) => (
0 ? "right" : "left", lineHeight: 1.2, }} > {h}
))}
)}
{rows.map((row, ri) => { const op = easeOutQuint(progressAt(frame, start + 14 + ri * 4, 16)); return (
{Array.from({ length: nCols }).map((_, ci) => { const raw = row[ci] ?? ""; const color = ci === highlightCol ? hlColor(raw) : undefined; return (
0 ? "right" : "left", fontVariantNumeric: "tabular-nums", overflowWrap: "anywhere", }} > {raw}
); })}
); })} {!rows.length && (
No entries — add data by editing this scene
)}
); };