Skip to main content
Glama
component_builder.py10 kB
""" TSX Component Builder - Generates Remotion React components from templates. Uses Jinja2 templates to generate type-safe TSX components. """ from pathlib import Path from typing import Any from jinja2 import Environment, FileSystemLoader from pydantic import BaseModel from ..themes.youtube_themes import YOUTUBE_THEMES from ..tokens.motion import MOTION_TOKENS from ..tokens.spacing import SPACING_TOKENS from ..tokens.typography import TYPOGRAPHY_TOKENS class ComponentBuilder: """Builds TSX components from templates and configurations.""" def __init__(self): """Initialize the component builder with Jinja2 environment.""" # Get template directory - now in components folder self.components_dir = Path(__file__).parent.parent / "components" # Also keep old template directory for backwards compatibility self.old_template_dir = Path(__file__).parent / "templates" # Create Jinja2 environment with multiple search paths search_paths = [str(self.components_dir)] if self.old_template_dir.exists(): search_paths.append(str(self.old_template_dir)) self.env = Environment( loader=FileSystemLoader(search_paths), trim_blocks=True, lstrip_blocks=True, autoescape=False, # Don't escape TSX code # Use [[ ]] for variables and [% %] for statements to avoid JSX {} conflicts variable_start_string="[[", variable_end_string="]]", block_start_string="[%", block_end_string="%]", ) # Add custom filters self.env.filters["to_camel_case"] = self._to_camel_case self.env.filters["to_pascal_case"] = self._to_pascal_case # Template categories for organized template discovery self.template_categories = [ "charts", "overlays", "layouts", "code", "animations", "content", "frames", "transitions", "text_animations", ] def _to_camel_case(self, snake_str: str) -> str: """Convert snake_case to camelCase.""" components = snake_str.split("_") return components[0] + "".join(x.title() for x in components[1:]) def _to_pascal_case(self, snake_str: str) -> str: """Convert snake_case to PascalCase.""" return "".join(x.title() for x in snake_str.split("_")) def _find_template(self, component_name: str) -> str: """ Find template file in component folders or old template structure. Args: component_name: Name of the component (e.g., "LowerThird") Returns: Template path relative to search paths """ # New structure: components/[category]/[Component]/template.tsx.j2 for category in self.template_categories: component_template = self.components_dir / category / component_name / "template.tsx.j2" if component_template.exists(): return f"{category}/{component_name}/template.tsx.j2" # Old structure: templates/[category]/[Component].tsx.j2 (backwards compatibility) template_filename = f"{component_name}.tsx.j2" # Try old root directory if self.old_template_dir.exists(): root_template = self.old_template_dir / template_filename if root_template.exists(): return template_filename # Try old category subdirectories for category in self.template_categories: category_template = self.old_template_dir / category / template_filename if category_template.exists(): return f"{category}/{template_filename}" # If not found, raise error raise ValueError(f"Template {template_filename} not found in any category") def build_component( self, component_name: str, config: dict[str, Any], theme_name: str = "tech" ) -> str: """ Build a TSX component from configuration. Args: component_name: Name of the component (e.g., "LowerThird") config: Component configuration dictionary theme_name: Theme to apply Returns: Generated TSX component code as string """ # Get theme (now returns a Pydantic Theme model) theme = YOUTUBE_THEMES.get(theme_name, YOUTUBE_THEMES["tech"]) assert theme is not None, f"Theme '{theme_name}' not found" # Find and get template try: template_path = self._find_template(component_name) template = self.env.get_template(template_path) except Exception as e: raise ValueError(f"Template not found for {component_name}: {e}") from e # Get font sizes for the theme's resolution resolution = theme.typography.default_resolution font_sizes = getattr(TYPOGRAPHY_TOKENS.font_sizes, resolution) # Build typography context with font_sizes for template compatibility # This maintains backwards compatibility with existing templates that expect typography.font_sizes typography_context = theme.typography.model_dump() typography_context["font_sizes"] = TYPOGRAPHY_TOKENS.font_sizes.model_dump(by_alias=True) typography_context["font_weights"] = TYPOGRAPHY_TOKENS.font_weights.model_dump() typography_context["line_heights"] = TYPOGRAPHY_TOKENS.line_heights.model_dump() typography_context["letter_spacing"] = TYPOGRAPHY_TOKENS.letter_spacing.model_dump() # Create Pydantic model for combined motion context # Combines full motion tokens with theme-specific defaults class CombinedMotionContext(BaseModel): """Combined motion tokens and theme defaults for template rendering.""" # Full motion tokens duration: Any # DurationTokens easing: Any # EasingTokens spring_configs: Any # SpringConfigs enter: Any # EnterExitTokens exit: Any # EnterExitTokens tempo: Any # TempoTokens platform_timing: Any # PlatformTiming # Theme-specific defaults default_spring: Any default_easing: Any default_duration: Any class Config: arbitrary_types_allowed = True motion_context = CombinedMotionContext( duration=MOTION_TOKENS.duration, easing=MOTION_TOKENS.easing, spring_configs=MOTION_TOKENS.spring_configs, enter=MOTION_TOKENS.enter, exit=MOTION_TOKENS.exit, tempo=MOTION_TOKENS.tempo, platform_timing=MOTION_TOKENS.platform_timing, default_spring=theme.motion.default_spring, default_easing=theme.motion.default_easing, default_duration=theme.motion.default_duration, ) # Render template - convert Pydantic models to dicts for Jinja2 template compatibility tsx_code = template.render( config=config, theme=theme.model_dump(), colors=theme.colors.model_dump(), typography=typography_context, motion=motion_context, # Now includes full motion tokens + theme defaults spacing=SPACING_TOKENS.model_dump(by_alias=True), font_sizes=font_sizes.model_dump(by_alias=True), # Also include global token singletons for convenience TYPOGRAPHY_TOKENS=TYPOGRAPHY_TOKENS, SPACING_TOKENS=SPACING_TOKENS, MOTION_TOKENS=MOTION_TOKENS, ) return tsx_code def build_lower_third( self, name: str, title: str | None = None, variant: str = "glass", position: str = "bottom_left", theme_name: str = "tech", ) -> str: """ Build a LowerThird component. Args: name: Main name/text title: Optional subtitle variant: Style variant (minimal, standard, glass, bold, animated) position: Screen position theme_name: Theme to apply Returns: TSX component code """ config = {"name": name, "title": title, "variant": variant, "position": position} return self.build_component("LowerThird", config, theme_name) def build_title_scene( self, text: str, subtitle: str | None = None, variant: str = "bold", animation: str = "fade_zoom", theme_name: str = "tech", ) -> str: """ Build a TitleScene component. Args: text: Main title text subtitle: Optional subtitle variant: Style variant (minimal, standard, bold, kinetic) animation: Animation style theme_name: Theme to apply Returns: TSX component code """ config = {"text": text, "subtitle": subtitle, "variant": variant, "animation": animation} return self.build_component("TitleScene", config, theme_name) def get_theme_styles(self, theme_name: str) -> str: """ Generate CSS-in-JS styles for a theme. Args: theme_name: Name of the theme Returns: JavaScript object with theme styles """ theme = YOUTUBE_THEMES.get(theme_name, YOUTUBE_THEMES["tech"]) assert theme is not None, f"Theme '{theme_name}' not found" colors = theme.colors styles = { "primary": colors.primary[0] if isinstance(colors.primary, list) else colors.primary, "accent": colors.accent[0] if isinstance(colors.accent, list) else colors.accent, "gradient": colors.gradient, "background": colors.background.model_dump(), "text": colors.text.model_dump(), } return f"export const themeStyles = {styles};"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server