/** * Merge meta.json `layout_prop_schema[layout].defaults` under stored layoutProps, * matching `backend/app/services/remotion.py` (`write_remotion_data`, ~983–1002). * * Keeps frontend preview / SceneEditModal in sync with the rendered MP4 for * chart colors, axis captions, example tables, font sizes, etc. */ export type LayoutPropSchema = Record }>; // Economist wordmark/dateline/teasers are LLM-authored content, never defaults — // mirror `_ECONOMIST_CONTENT_SKIP_KEYS` in backend/app/services/remotion.py so the // preview never injects the sample "The Brief" wordmark onto the cover / sign-off. const ECONOMIST_CONTENT_SKIP_KEYS = new Set(["wordmark", "dateline", "teasers"]); function resolveDefaultsForAspect( defaults: Record, aspectRatio: string, ): Record { const ar = aspectRatio === "portrait" ? "portrait" : "landscape"; const resolved: Record = {}; for (const [key, value] of Object.entries(defaults)) { if ( value && typeof value === "object" && !Array.isArray(value) && "portrait" in value && "landscape" in value ) { const responsive = value as { portrait?: unknown; landscape?: unknown }; resolved[key] = responsive[ar] ?? responsive.landscape; } else { resolved[key] = value; } } return resolved; } /** * Apply layout schema defaults under `layoutProps` (stored props win on conflict). */ export function mergeLayoutSchemaDefaults( layoutProps: Record, layoutId: string | null | undefined, schema: LayoutPropSchema | null | undefined, aspectRatio = "landscape", templateId?: string | null, ): Record { if (!layoutId || !schema || Object.keys(schema).length === 0) return layoutProps; const defaults = schema[layoutId]?.defaults; if (!defaults || Object.keys(defaults).length === 0) return layoutProps; const resolved = resolveDefaultsForAspect(defaults, aspectRatio); // Drop content-only defaults the renderer also refuses to inject, so the // preview matches the MP4 (e.g. economist never auto-stamps a brand wordmark). if (String(templateId ?? "").toLowerCase() === "economist") { for (const key of ECONOMIST_CONTENT_SKIP_KEYS) delete resolved[key]; } const merged = { ...resolved, ...layoutProps }; // Keep example chart data when stored chartTable has no rows (legacy partial merge). const existingTable = layoutProps.chartTable; const existingTableHasRows = existingTable && typeof existingTable === "object" && Array.isArray((existingTable as { rows?: unknown }).rows) && ((existingTable as { rows: unknown[] }).rows.length > 0); if (!existingTableHasRows && resolved.chartTable && typeof resolved.chartTable === "object") { merged.chartTable = resolved.chartTable; } const existingTickerTable = layoutProps.tickerTable; const existingTickerTableHasRows = existingTickerTable && typeof existingTickerTable === "object" && Array.isArray((existingTickerTable as { rows?: unknown }).rows) && ((existingTickerTable as { rows: unknown[] }).rows.some((row) => Array.isArray(row) && row.some((cell) => String(cell ?? "").trim() !== ""), )); if (!existingTickerTableHasRows && resolved.tickerTable && typeof resolved.tickerTable === "object") { merged.tickerTable = resolved.tickerTable; } if (!("chartType" in layoutProps) && typeof resolved.chartType === "string") { merged.chartType = resolved.chartType; } return merged; }