mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-05 15:20:40 +08:00
Implementation spec: governance, decision intelligence, theme system, and E2E bug fixes
Implements the 2026-04-02 transformation spec (Phases 1-8) and fixes all critical bugs found during 5-pipeline E2E testing. Governance & Decision Intelligence: - Pipeline-specific stage order in checkpoint (replaces global STAGES list) - Provider scoring engine (lib/scoring.py) with 7-dimension weighted ranking - Decision log artifact enforced at proposal/idea stage across all 10 pipelines - Delivery promise classifier prevents silent motion-to-still downgrades - Structured shot language in scene_plan schema (camera, lens, lighting, DOF) - Variation checker and slideshow risk scorer block samey output before render - Creative intake, capability extension, and creative-intake meta skills - Final self-review artifact with 5 mandatory checks before presenting output - Source media review contract for user-supplied footage Render & Theme System: - Remotion AnimatedBackground now derives colors from playbook (no more hardcoded dark blue fintech gradient on every video) - video_compose builds custom ThemeConfig from playbook YAML colors/fonts — custom playbooks flow through to Remotion automatically - Explainer component wires theme to all child components (charts, cards, etc.) - resolveAsset() handles absolute paths on Windows/Unix via file:// URIs - RENDERER_FAMILY_MAP synced with actual Remotion compositions Critical Bug Fixes: - Windows npx subprocess: run_command() resolves .cmd wrappers via shutil.which() - Silent renderer downgrade: Remotion failure now returns explicit error with options instead of silently falling back to FFmpeg - .env inline comment parsing strips trailing # comments from API keys - concat_path UnboundLocalError in video_compose finally block - audio_mixer and showcase_card capture=True kwarg bug - Selector estimate_cost() calls fixed (_select_tool -> _select_best_tool) - asset_manifest schema expanded with provider, license, subtype fields - screen-demo subtitle_gen moved from required to optional tools - Duration drift detection in post-render final review (>25% warns)
This commit is contained in:
@@ -12,13 +12,18 @@ import {
|
||||
} from "remotion";
|
||||
import { loadFont } from "@remotion/google-fonts/SpaceGrotesk";
|
||||
|
||||
// Resolve asset path — use staticFile() for local paths, passthrough URLs
|
||||
// Resolve asset path — handle URLs, absolute paths (Windows/Unix), and public/ relative paths
|
||||
function resolveAsset(src: string): string {
|
||||
if (src.startsWith("http://") || src.startsWith("https://") || src.startsWith("data:")) {
|
||||
return src;
|
||||
}
|
||||
// Strip any file:// prefix
|
||||
const clean = src.replace(/^file:\/\/\/?/, "");
|
||||
// Absolute paths (Unix: /foo, Windows: C:\foo or C:/foo) — convert to file:// URI
|
||||
// staticFile() only accepts relative paths within public/, so absolute paths must bypass it
|
||||
if (clean.startsWith("/") || /^[A-Za-z]:[\\/]/.test(clean)) {
|
||||
return `file:///${clean.replace(/\\/g, "/")}`;
|
||||
}
|
||||
return staticFile(clean);
|
||||
}
|
||||
import { TextCard } from "./components/TextCard";
|
||||
@@ -37,6 +42,7 @@ import { HeroTitle } from "./components/HeroTitle";
|
||||
import { AnimeScene } from "./components/AnimeScene";
|
||||
import type { CameraMotion } from "./components/AnimeScene";
|
||||
import type { ParticleType } from "./components/ParticleOverlay";
|
||||
import { resolveTheme, type ThemeConfig, DEFAULT_THEME } from "./Root";
|
||||
|
||||
// Load Space Grotesk font for cinematic typography
|
||||
const { fontFamily } = loadFont("normal", {
|
||||
@@ -48,45 +54,84 @@ const { fontFamily } = loadFont("normal", {
|
||||
// Animated Background — Gradient Mesh + Floating Orbs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AnimatedBackground: React.FC<{ style?: "fintech" | "default" }> = ({
|
||||
style: bgStyle = "default",
|
||||
}) => {
|
||||
// Parse hex color to RGB components
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const clean = hex.replace("#", "");
|
||||
const bigint = parseInt(clean.length === 3
|
||||
? clean.split("").map(c => c + c).join("")
|
||||
: clean, 16);
|
||||
return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 };
|
||||
}
|
||||
|
||||
// Detect if a color is "light" (for choosing grid/overlay treatment)
|
||||
function isLightColor(hex: string): boolean {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 128;
|
||||
}
|
||||
|
||||
// Darken/lighten a color by mixing toward black or white
|
||||
function shiftColor(hex: string, amount: number): string {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
|
||||
if (amount < 0) {
|
||||
// Darken
|
||||
const f = 1 + amount;
|
||||
return `rgb(${clamp(r * f)}, ${clamp(g * f)}, ${clamp(b * f)})`;
|
||||
}
|
||||
// Lighten
|
||||
return `rgb(${clamp(r + (255 - r) * amount)}, ${clamp(g + (255 - g) * amount)}, ${clamp(b + (255 - b) * amount)})`;
|
||||
}
|
||||
|
||||
const AnimatedBackground: React.FC<{ theme: ThemeConfig }> = ({ theme }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, durationInFrames, width, height } = useVideoConfig();
|
||||
const progress = frame / durationInFrames;
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
|
||||
const bg = theme.backgroundColor;
|
||||
const primary = theme.primaryColor;
|
||||
const accent = theme.accentColor;
|
||||
const surface = theme.surfaceColor;
|
||||
const light = isLightColor(bg);
|
||||
|
||||
// Slow-moving gradient angles
|
||||
const angle1 = 135 + Math.sin(frame / (fps * 8)) * 30;
|
||||
const angle2 = 225 + Math.cos(frame / (fps * 6)) * 25;
|
||||
|
||||
// Color stops shift over time
|
||||
const shift = Math.sin(frame / (fps * 12)) * 0.15;
|
||||
// Build gradient from theme colors instead of hardcoded dark blue
|
||||
const { r: bgR, g: bgG, b: bgB } = hexToRgb(bg);
|
||||
const { r: priR, g: priG, b: priB } = hexToRgb(primary);
|
||||
const { r: accR, g: accG, b: accB } = hexToRgb(accent);
|
||||
|
||||
const gradient = `
|
||||
radial-gradient(ellipse at ${30 + Math.sin(frame / (fps * 10)) * 20}% ${40 + Math.cos(frame / (fps * 8)) * 20}%,
|
||||
rgba(15, 23, 60, 1) 0%, transparent 60%),
|
||||
rgba(${priR}, ${priG}, ${priB}, 0.15) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at ${70 + Math.cos(frame / (fps * 7)) * 20}% ${60 + Math.sin(frame / (fps * 9)) * 25}%,
|
||||
rgba(30, 10, 60, 0.8) 0%, transparent 55%),
|
||||
radial-gradient(ellipse at ${50 + Math.sin(frame / (fps * 14)) * 30}% ${20 + Math.cos(frame / (fps * 11)) * 15}%,
|
||||
rgba(0, 40, 60, 0.6) 0%, transparent 50%),
|
||||
linear-gradient(${angle1}deg, #060918 0%, #0B1026 40%, #0F0A2E 70%, #080D1F 100%)
|
||||
rgba(${accR}, ${accG}, ${accB}, 0.1) 0%, transparent 55%),
|
||||
linear-gradient(${angle1}deg, ${bg} 0%, ${shiftColor(bg, light ? -0.05 : 0.05)} 40%, ${surface} 70%, ${bg} 100%)
|
||||
`;
|
||||
|
||||
// Floating orbs
|
||||
// Floating orbs — derived from theme chart colors with low opacity
|
||||
const orbColors = theme.chartColors.slice(0, 5);
|
||||
const orbOpacity = light ? 0.06 : 0.08;
|
||||
const orbs = [
|
||||
{ x: 20, y: 30, size: 300, color: "rgba(34, 211, 238, 0.08)", speedX: 7, speedY: 11 },
|
||||
{ x: 70, y: 60, size: 250, color: "rgba(139, 92, 246, 0.1)", speedX: 9, speedY: 8 },
|
||||
{ x: 40, y: 80, size: 200, color: "rgba(16, 185, 129, 0.07)", speedX: 13, speedY: 6 },
|
||||
{ x: 80, y: 20, size: 350, color: "rgba(245, 158, 11, 0.06)", speedX: 11, speedY: 14 },
|
||||
{ x: 10, y: 70, size: 180, color: "rgba(236, 72, 153, 0.05)", speedX: 8, speedY: 10 },
|
||||
{ x: 20, y: 30, size: 300, color: orbColors[0] || primary, speedX: 7, speedY: 11 },
|
||||
{ x: 70, y: 60, size: 250, color: orbColors[1] || accent, speedX: 9, speedY: 8 },
|
||||
{ x: 40, y: 80, size: 200, color: orbColors[2] || primary, speedX: 13, speedY: 6 },
|
||||
{ x: 80, y: 20, size: 350, color: orbColors[3] || accent, speedX: 11, speedY: 14 },
|
||||
{ x: 10, y: 70, size: 180, color: orbColors[4] || primary, speedX: 8, speedY: 10 },
|
||||
];
|
||||
|
||||
// Grid and overlay colors adapt to light vs dark backgrounds
|
||||
const gridColor = light ? "rgba(0,0,0,0.03)" : "rgba(255,255,255,0.02)";
|
||||
const fadeColor = light
|
||||
? `rgba(${bgR},${bgG},${bgB},0.2)`
|
||||
: `rgba(${bgR},${bgG},${bgB},0.4)`;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: gradient }}>
|
||||
{/* Floating glow orbs */}
|
||||
{orbs.map((orb, i) => {
|
||||
const ox = orb.x + Math.sin(frame / (fps * orb.speedX)) * 15;
|
||||
const oy = orb.y + Math.cos(frame / (fps * orb.speedY)) * 12;
|
||||
const { r, g, b } = hexToRgb(orb.color);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -97,7 +142,7 @@ const AnimatedBackground: React.FC<{ style?: "fintech" | "default" }> = ({
|
||||
width: orb.size,
|
||||
height: orb.size,
|
||||
borderRadius: "50%",
|
||||
background: orb.color,
|
||||
background: `rgba(${r}, ${g}, ${b}, ${orbOpacity})`,
|
||||
filter: `blur(${orb.size * 0.4}px)`,
|
||||
transform: "translate(-50%, -50%)",
|
||||
willChange: "transform",
|
||||
@@ -112,8 +157,8 @@ const AnimatedBackground: React.FC<{ style?: "fintech" | "default" }> = ({
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)
|
||||
linear-gradient(${gridColor} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${gridColor} 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "60px 60px",
|
||||
opacity: 0.5 + Math.sin(frame / (fps * 20)) * 0.2,
|
||||
@@ -128,7 +173,7 @@ const AnimatedBackground: React.FC<{ style?: "fintech" | "default" }> = ({
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "30%",
|
||||
background: "linear-gradient(to bottom, rgba(6,9,24,0.4), transparent)",
|
||||
background: `linear-gradient(to bottom, ${fadeColor}, transparent)`,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
@@ -427,7 +472,7 @@ const BackgroundImageLayer: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
const SceneRenderer: React.FC<{ cut: Cut; theme: ThemeConfig }> = ({ cut, theme }) => {
|
||||
// Wrap component with background image if specified
|
||||
const maybeWrapWithBgImage = (element: React.ReactElement) => {
|
||||
if (cut.backgroundImage) {
|
||||
@@ -445,26 +490,29 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
|
||||
// Resolve the scene element based on cut type, then wrap with backgroundImage if set
|
||||
// Use transparent bg so the animated gradient background shows through
|
||||
const rawBg = cut.backgroundImage ? "transparent" : cut.backgroundColor;
|
||||
const bgColor = (rawBg === "#0F172A" || rawBg === "#0f172a") ? "transparent" : rawBg;
|
||||
// When no explicit backgroundColor on the cut, inherit from theme
|
||||
const rawBg = cut.backgroundImage ? "transparent" : (cut.backgroundColor || theme.surfaceColor);
|
||||
const bgColor = (rawBg === theme.backgroundColor || rawBg === "#0F172A" || rawBg === "#0f172a") ? "transparent" : rawBg;
|
||||
const textColor = cut.color || theme.textColor;
|
||||
const accent = cut.accentColor || theme.accentColor;
|
||||
|
||||
// Explicit component types
|
||||
// Explicit component types — use theme-derived defaults for colors
|
||||
if (cut.type === "text_card" && cut.text) {
|
||||
return maybeWrapWithBgImage(
|
||||
<TextCard text={cut.text} fontSize={cut.fontSize} color={cut.color} backgroundColor={bgColor} />
|
||||
<TextCard text={cut.text} fontSize={cut.fontSize} color={textColor} backgroundColor={bgColor} />
|
||||
);
|
||||
}
|
||||
if (cut.type === "stat_card" && cut.stat) {
|
||||
return maybeWrapWithBgImage(
|
||||
<StatCard stat={cut.stat} subtitle={cut.subtitle} accentColor={cut.accentColor} backgroundColor={bgColor} />
|
||||
<StatCard stat={cut.stat} subtitle={cut.subtitle} accentColor={accent} backgroundColor={bgColor} />
|
||||
);
|
||||
}
|
||||
if (cut.type === "callout" && cut.text) {
|
||||
return maybeWrapWithBgImage(
|
||||
<CalloutBox
|
||||
text={cut.text} type={cut.callout_type} title={cut.title}
|
||||
borderColor={cut.accentColor} backgroundColor={cut.backgroundColor}
|
||||
textColor={cut.color} containerBackgroundColor={bgColor}
|
||||
borderColor={accent} backgroundColor={cut.backgroundColor || theme.surfaceColor}
|
||||
textColor={textColor} containerBackgroundColor={bgColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -473,7 +521,7 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
<ComparisonCard
|
||||
leftLabel={cut.leftLabel} rightLabel={cut.rightLabel}
|
||||
leftValue={cut.leftValue} rightValue={cut.rightValue}
|
||||
title={cut.title} backgroundColor={bgColor} textColor={cut.color}
|
||||
title={cut.title} backgroundColor={bgColor} textColor={textColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -483,11 +531,11 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Chart types ---
|
||||
// --- Chart types — use theme.chartColors as default palette ---
|
||||
if (cut.type === "bar_chart" && cut.chartData) {
|
||||
return maybeWrapWithBgImage(
|
||||
<BarChart
|
||||
data={cut.chartData} title={cut.title} colors={cut.chartColors}
|
||||
data={cut.chartData} title={cut.title} colors={cut.chartColors || theme.chartColors}
|
||||
animationStyle={(cut.chartAnimation as any) || "grow-up"}
|
||||
showGrid={cut.showGrid} showValues={cut.showValues} backgroundColor={bgColor}
|
||||
/>
|
||||
@@ -496,7 +544,7 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
if (cut.type === "line_chart" && cut.chartSeries) {
|
||||
return maybeWrapWithBgImage(
|
||||
<LineChart
|
||||
series={cut.chartSeries} title={cut.title} colors={cut.chartColors}
|
||||
series={cut.chartSeries} title={cut.title} colors={cut.chartColors || theme.chartColors}
|
||||
animationStyle={(cut.chartAnimation as any) || "draw"}
|
||||
showGrid={cut.showGrid} showMarkers={cut.showMarkers} showLegend={cut.showLegend}
|
||||
xLabel={cut.xLabel} yLabel={cut.yLabel} backgroundColor={bgColor}
|
||||
@@ -506,7 +554,7 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
if (cut.type === "pie_chart" && cut.chartData) {
|
||||
return maybeWrapWithBgImage(
|
||||
<PieChart
|
||||
data={cut.chartData} title={cut.title} colors={cut.chartColors}
|
||||
data={cut.chartData} title={cut.title} colors={cut.chartColors || theme.chartColors}
|
||||
animationStyle={(cut.chartAnimation as any) || "expand"}
|
||||
donut={cut.donut} centerLabel={cut.centerLabel} centerValue={cut.centerValue}
|
||||
showLegend={cut.showLegend} backgroundColor={bgColor}
|
||||
@@ -517,7 +565,7 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
return maybeWrapWithBgImage(
|
||||
<KPIGrid
|
||||
metrics={cut.chartData} title={cut.title} columns={cut.columns}
|
||||
colors={cut.chartColors} animationStyle={(cut.chartAnimation as any) || "count-up"}
|
||||
colors={cut.chartColors || theme.chartColors} animationStyle={(cut.chartAnimation as any) || "count-up"}
|
||||
backgroundColor={bgColor}
|
||||
/>
|
||||
);
|
||||
@@ -526,7 +574,7 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
return maybeWrapWithBgImage(
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: bgColor || "#FFFFFF",
|
||||
background: bgColor || theme.surfaceColor,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: "80px 120px",
|
||||
}}
|
||||
@@ -534,16 +582,16 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
{cut.title && (
|
||||
<div style={{
|
||||
position: "absolute", top: 120, fontSize: 48, fontWeight: 700,
|
||||
color: "#1F2937", textAlign: "center", width: "100%",
|
||||
color: textColor, textAlign: "center", width: "100%",
|
||||
}}>
|
||||
{cut.title}
|
||||
</div>
|
||||
)}
|
||||
<ProgressBar
|
||||
progress={cut.progress} label={cut.progressLabel}
|
||||
color={cut.progressColor || cut.accentColor}
|
||||
color={cut.progressColor || accent}
|
||||
animationStyle={(cut.progressAnimation as any) || "fill"}
|
||||
segments={cut.progressSegments} backgroundColor={cut.backgroundColor}
|
||||
segments={cut.progressSegments} backgroundColor={cut.backgroundColor || theme.surfaceColor}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
@@ -585,7 +633,7 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
|
||||
}
|
||||
|
||||
// No source, no type — render as text card with cut id as fallback
|
||||
return <TextCard text={cut.text || cut.id} />;
|
||||
return <TextCard text={cut.text || cut.id} color={textColor} backgroundColor={bgColor} />;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -623,18 +671,17 @@ const OverlayRenderer: React.FC<{ overlay: Overlay }> = ({ overlay }) => {
|
||||
// Main composition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const Explainer: React.FC<ExplainerProps> = ({
|
||||
cuts,
|
||||
overlays,
|
||||
captions,
|
||||
audio,
|
||||
}) => {
|
||||
export const Explainer: React.FC<ExplainerProps> = (props) => {
|
||||
const { cuts, overlays, captions, audio } = props;
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
|
||||
// Resolve theme from props — playbook name, theme name, or custom themeConfig
|
||||
const theme = resolveTheme(props as Record<string, unknown>);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: "#060918", fontFamily }}>
|
||||
{/* Layer 0: Animated gradient background */}
|
||||
<AnimatedBackground style="fintech" />
|
||||
<AbsoluteFill style={{ background: theme.backgroundColor, fontFamily: theme.headingFont || fontFamily }}>
|
||||
{/* Layer 0: Animated gradient background — driven by theme */}
|
||||
<AnimatedBackground theme={theme} />
|
||||
|
||||
{/* Layer 1: Visual scenes */}
|
||||
{cuts.map((cut) => {
|
||||
@@ -643,7 +690,7 @@ export const Explainer: React.FC<ExplainerProps> = ({
|
||||
|
||||
return (
|
||||
<Sequence key={cut.id} from={from} durationInFrames={duration}>
|
||||
<SceneRenderer cut={cut} />
|
||||
<SceneRenderer cut={cut} theme={theme} />
|
||||
</Sequence>
|
||||
);
|
||||
})}
|
||||
@@ -668,8 +715,8 @@ export const Explainer: React.FC<ExplainerProps> = ({
|
||||
words={captions}
|
||||
wordsPerPage={6}
|
||||
fontSize={42}
|
||||
highlightColor="#22D3EE"
|
||||
backgroundColor="rgba(15, 23, 42, 0.7)"
|
||||
highlightColor={theme.captionHighlightColor}
|
||||
backgroundColor={theme.captionBackgroundColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,6 +7,109 @@ import {
|
||||
import { signalFromTomorrowWithMusicFixture } from "./cinematic/fixtures";
|
||||
import { TalkingHead, TalkingHeadProps } from "./TalkingHead";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme System — prevents every video from looking like dark fintech
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ThemeConfig {
|
||||
primaryColor: string;
|
||||
accentColor: string;
|
||||
backgroundColor: string;
|
||||
surfaceColor: string;
|
||||
textColor: string;
|
||||
mutedTextColor: string;
|
||||
headingFont: string;
|
||||
bodyFont: string;
|
||||
monoFont: string;
|
||||
chartColors: string[];
|
||||
springConfig: { damping: number; stiffness: number; mass: number };
|
||||
transitionDuration: number;
|
||||
captionHighlightColor: string;
|
||||
captionBackgroundColor: string;
|
||||
}
|
||||
|
||||
export const THEMES: Record<string, ThemeConfig> = {
|
||||
"clean-professional": {
|
||||
primaryColor: "#2563EB",
|
||||
accentColor: "#F59E0B",
|
||||
backgroundColor: "#FFFFFF",
|
||||
surfaceColor: "#F9FAFB",
|
||||
textColor: "#1F2937",
|
||||
mutedTextColor: "#6B7280",
|
||||
headingFont: "Inter",
|
||||
bodyFont: "Inter",
|
||||
monoFont: "JetBrains Mono",
|
||||
chartColors: ["#2563EB", "#F59E0B", "#10B981", "#8B5CF6", "#EC4899", "#06B6D4"],
|
||||
springConfig: { damping: 20, stiffness: 120, mass: 1 },
|
||||
transitionDuration: 0.4,
|
||||
captionHighlightColor: "#2563EB",
|
||||
captionBackgroundColor: "rgba(255, 255, 255, 0.85)",
|
||||
},
|
||||
"flat-motion-graphics": {
|
||||
primaryColor: "#7C3AED",
|
||||
accentColor: "#EC4899",
|
||||
backgroundColor: "#0F172A",
|
||||
surfaceColor: "#1E293B",
|
||||
textColor: "#F8FAFC",
|
||||
mutedTextColor: "#94A3B8",
|
||||
headingFont: "Space Grotesk",
|
||||
bodyFont: "Space Grotesk",
|
||||
monoFont: "Fira Code",
|
||||
chartColors: ["#7C3AED", "#EC4899", "#06B6D4", "#F59E0B", "#10B981", "#EF4444"],
|
||||
springConfig: { damping: 12, stiffness: 80, mass: 1 },
|
||||
transitionDuration: 0.3,
|
||||
captionHighlightColor: "#22D3EE",
|
||||
captionBackgroundColor: "rgba(15, 23, 42, 0.75)",
|
||||
},
|
||||
"minimalist-diagram": {
|
||||
primaryColor: "#1A1A2E",
|
||||
accentColor: "#E94560",
|
||||
backgroundColor: "#FAFAFA",
|
||||
surfaceColor: "#FFFFFF",
|
||||
textColor: "#1A1A2E",
|
||||
mutedTextColor: "#6B7280",
|
||||
headingFont: "IBM Plex Sans",
|
||||
bodyFont: "IBM Plex Sans",
|
||||
monoFont: "IBM Plex Mono",
|
||||
chartColors: ["#E94560", "#1A1A2E", "#0F3460", "#9CA3AF"],
|
||||
springConfig: { damping: 25, stiffness: 150, mass: 1 },
|
||||
transitionDuration: 0.5,
|
||||
captionHighlightColor: "#E94560",
|
||||
captionBackgroundColor: "rgba(250, 250, 250, 0.9)",
|
||||
},
|
||||
"anime-ghibli": {
|
||||
primaryColor: "#2D5016",
|
||||
accentColor: "#FFB347",
|
||||
backgroundColor: "#0A0A1A",
|
||||
surfaceColor: "#1A2332",
|
||||
textColor: "#F0E6D3",
|
||||
mutedTextColor: "#A8957E",
|
||||
headingFont: "Noto Serif JP",
|
||||
bodyFont: "Noto Sans",
|
||||
monoFont: "Fira Code",
|
||||
chartColors: ["#FFB347", "#2D5016", "#FF6B9D", "#A8E6CF", "#6B4C8A", "#E8927C"],
|
||||
springConfig: { damping: 18, stiffness: 60, mass: 1 },
|
||||
transitionDuration: 1.0,
|
||||
captionHighlightColor: "#FFB347",
|
||||
captionBackgroundColor: "rgba(10, 10, 26, 0.8)",
|
||||
},
|
||||
};
|
||||
|
||||
// Default theme when none is specified — uses the existing dark style for backwards compatibility
|
||||
export const DEFAULT_THEME = THEMES["flat-motion-graphics"];
|
||||
|
||||
export function resolveTheme(props: Record<string, unknown>): ThemeConfig {
|
||||
const themeName = (props.theme as string) || (props.playbook as string);
|
||||
if (themeName && THEMES[themeName]) {
|
||||
return THEMES[themeName];
|
||||
}
|
||||
// Allow custom theme passed as full object
|
||||
if (props.themeConfig && typeof props.themeConfig === "object") {
|
||||
return { ...DEFAULT_THEME, ...(props.themeConfig as Partial<ThemeConfig>) };
|
||||
}
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction<ExplainerProps> = async ({
|
||||
props,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user