""" Design System for AI-Generated Social Posts Defines typography scales, color palettes, spacing rules, and layout templates. Ensures consistent, professional-looking posts across all platforms. """ from enum import Enum from typing import NamedTuple, Dict, List, Tuple from dataclasses import dataclass # ───────────────────────────────────────────────────────────────────────────── # TYPOGRAPHY SCALE (Following design system best practices) # ───────────────────────────────────────────────────────────────────────────── class TypographyScale(NamedTuple): """Typography scale for different text elements.""" headline_xl: int = 96 # Main hook/title (1080px width) headline_lg: int = 72 # Large secondary title headline_md: int = 56 # Medium headline headline_sm: int = 44 # Small headline body_lg: int = 40 # Large body text body_md: int = 32 # Medium body (primary) body_sm: int = 24 # Small body (secondary) caption: int = 20 # Captions/hashtags tiny: int = 16 # Micro text TYPOGRAPHY = TypographyScale() # ───────────────────────────────────────────────────────────────────────────── # SPACING SYSTEM (8px base unit) # ───────────────────────────────────────────────────────────────────────────── class Spacing(NamedTuple): """8px-based spacing scale.""" xs: int = 8 # 8px sm: int = 16 # 16px md: int = 24 # 24px lg: int = 32 # 32px xl: int = 48 # 48px xxl: int = 64 # 64px xxxl: int = 80 # 80px SPACING = Spacing() # ───────────────────────────────────────────────────────────────────────────── # LINE HEIGHT SCALE (Better readability) # ───────────────────────────────────────────────────────────────────────────── LINE_HEIGHT = { "tight": 1.1, # Headlines "normal": 1.3, # Body text (optimal for reading) "loose": 1.5, # Large body text "extra_loose": 1.8, # Captions } # ───────────────────────────────────────────────────────────────────────────── # COLOR PALETTES (Instagram-optimized + high contrast) # ───────────────────────────────────────────────────────────────────────────── @dataclass class ColorPalette: """A complete color palette for a post.""" name: str bg_top: Tuple[int, int, int] bg_bottom: Tuple[int, int, int] text_primary: Tuple[int, int, int] text_secondary: Tuple[int, int, int] accent_primary: Tuple[int, int, int] accent_secondary: Tuple[int, int, int] @property def contrast_text(self) -> Tuple[int, int, int]: """Get high-contrast text color.""" # Calculate luminance r, g, b = self.bg_top luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 return (20, 20, 20) if luminance > 0.5 else (255, 255, 255) # Professional Instagram-style palettes PALETTES = { # Modern Minimal "minimal_white": ColorPalette( name="Minimal White", bg_top=(255, 255, 255), bg_bottom=(248, 248, 248), text_primary=(18, 18, 18), text_secondary=(100, 100, 100), accent_primary=(0, 122, 255), accent_secondary=(255, 149, 0), ), # Bold Dark "dark_navy": ColorPalette( name="Dark Navy", bg_top=(8, 16, 32), bg_bottom=(16, 24, 48), text_primary=(255, 255, 255), text_secondary=(200, 200, 200), accent_primary=(64, 224, 255), accent_secondary=(255, 100, 200), ), # Warm Gradient "warm_sunset": ColorPalette( name="Warm Sunset", bg_top=(255, 189, 89), bg_bottom=(255, 140, 80), text_primary=(255, 255, 255), text_secondary=(240, 240, 240), accent_primary=(255, 255, 255), accent_secondary=(220, 60, 60), ), # Cool Modern "cool_gradient": ColorPalette( name="Cool Gradient", bg_top=(16, 48, 100), bg_bottom=(60, 100, 200), text_primary=(255, 255, 255), text_secondary=(200, 220, 255), accent_primary=(100, 255, 218), accent_secondary=(255, 200, 100), ), # Premium Dark "premium_black": ColorPalette( name="Premium Black", bg_top=(12, 12, 12), bg_bottom=(24, 24, 24), text_primary=(255, 255, 255), text_secondary=(180, 180, 180), accent_primary=(255, 192, 0), accent_secondary=(0, 200, 255), ), # Organic Green "organic_green": ColorPalette( name="Organic Green", bg_top=(240, 248, 245), bg_bottom=(220, 240, 235), text_primary=(24, 64, 48), text_secondary=(80, 120, 100), accent_primary=(34, 177, 76), accent_secondary=(255, 140, 0), ), } # ───────────────────────────────────────────────────────────────────────────── # ASPECT RATIOS FOR DIFFERENT PLATFORMS # ───────────────────────────────────────────────────────────────────────────── @dataclass class AspectRatio: """Social media aspect ratio definition.""" name: str width: int height: int platform: str @property def ratio(self) -> float: return self.width / self.height ASPECT_RATIOS = { "instagram_square": AspectRatio("Instagram Square", 1080, 1080, "instagram"), "instagram_feed": AspectRatio("Instagram Feed", 1080, 1350, "instagram"), "instagram_story": AspectRatio("Instagram Story", 1080, 1920, "instagram"), "tiktok": AspectRatio("TikTok", 1080, 1920, "tiktok"), "linkedin": AspectRatio("LinkedIn", 1200, 627, "linkedin"), "twitter": AspectRatio("Twitter/X", 1200, 675, "twitter"), } # ───────────────────────────────────────────────────────────────────────────── # LAYOUT TEMPLATES (Hook-first rendering) # ───────────────────────────────────────────────────────────────────────────── class LayoutType(str, Enum): """Different layout templates for posts.""" TYPOGRAPHY_BOLD = "typography_bold" # Text-only, maximum impact TYPOGRAPHY_MINIMAL = "typography_minimal" # Clean, simple text card EDITORIAL_PHOTO = "editorial_photo" # Full-bleed photo + text overlay SPLIT_HALF = "split_half" # 50/50 image + text IMAGE_FOCUS = "image_focus" # Image-dominant with subtle text CAROUSEL_SLIDE = "carousel_slide" # For carousel posts INFOGRAPHIC = "infographic" # Data/stats focused QUOTE_CARD = "quote_card" # Famous quote style @dataclass class LayoutTemplate: """Definition of a layout template.""" layout_type: LayoutType name: str description: str best_for: List[str] # Content types: ["short_text", "long_text", "stats", "quote"] uses_image: bool margin: int headline_size: int body_size: int line_height_ratio: float aspect_ratio: AspectRatio @property def inner_width(self) -> int: return self.aspect_ratio.width - (2 * self.margin) # Template definitions LAYOUT_TEMPLATES = { LayoutType.TYPOGRAPHY_BOLD: LayoutTemplate( layout_type=LayoutType.TYPOGRAPHY_BOLD, name="Bold Typography", description="Maximum impact text card with gradient background", best_for=["short_text", "quote", "hook"], uses_image=False, margin=64, headline_size=96, body_size=40, line_height_ratio=1.15, aspect_ratio=ASPECT_RATIOS["instagram_square"], ), LayoutType.TYPOGRAPHY_MINIMAL: LayoutTemplate( layout_type=LayoutType.TYPOGRAPHY_MINIMAL, name="Minimal Typography", description="Clean, spacious text card for readability", best_for=["text", "education", "thoughts"], uses_image=False, margin=80, headline_size=72, body_size=36, line_height_ratio=1.3, aspect_ratio=ASPECT_RATIOS["instagram_square"], ), LayoutType.EDITORIAL_PHOTO: LayoutTemplate( layout_type=LayoutType.EDITORIAL_PHOTO, name="Editorial Photo", description="Full-bleed photo with dark overlay and text", best_for=["story", "visual", "announcement"], uses_image=True, margin=60, headline_size=80, body_size=36, line_height_ratio=1.15, aspect_ratio=ASPECT_RATIOS["instagram_feed"], ), LayoutType.SPLIT_HALF: LayoutTemplate( layout_type=LayoutType.SPLIT_HALF, name="Split Half", description="50/50 split between image and text", best_for=["product", "comparison", "before_after"], uses_image=True, margin=48, headline_size=56, body_size=32, line_height_ratio=1.3, aspect_ratio=ASPECT_RATIOS["instagram_feed"], ), LayoutType.IMAGE_FOCUS: LayoutTemplate( layout_type=LayoutType.IMAGE_FOCUS, name="Image Focus", description="Image-dominant with subtle text badge", best_for=["visual", "design", "lifestyle"], uses_image=True, margin=40, headline_size=48, body_size=28, line_height_ratio=1.25, aspect_ratio=ASPECT_RATIOS["instagram_square"], ), LayoutType.INFOGRAPHIC: LayoutTemplate( layout_type=LayoutType.INFOGRAPHIC, name="Infographic", description="Stats and data-focused layout", best_for=["stats", "data", "comparison"], uses_image=False, margin=72, headline_size=64, body_size=36, line_height_ratio=1.4, aspect_ratio=ASPECT_RATIOS["instagram_square"], ), LayoutType.QUOTE_CARD: LayoutTemplate( layout_type=LayoutType.QUOTE_CARD, name="Quote Card", description="Famous quote or standalone thought", best_for=["quote", "inspiration", "thought"], uses_image=False, margin=64, headline_size=88, body_size=36, line_height_ratio=1.2, aspect_ratio=ASPECT_RATIOS["instagram_square"], ), } # ───────────────────────────────────────────────────────────────────────────── # CONTENT TYPE DETECTION # ───────────────────────────────────────────────────────────────────────────── def detect_content_type(text: str, has_image: bool = False) -> str: """Detect content type from text for layout selection.""" text_lower = text.lower() word_count = len(text.split()) # Quote detection if any(quote in text_lower for quote in ['"', '"', '"', "'", "—"]): return "quote" # Stats detection if any(stat_marker in text_lower for stat_marker in ["#", "%", "$", "x", "times", "increase"]): return "stats" # Length-based if word_count <= 10: return "short_text" elif word_count <= 30: return "text" else: return "long_text" # ───────────────────────────────────────────────────────────────────────────── # ACCESSIBILITY & CONTRAST # ───────────────────────────────────────────────────────────────────────────── def calculate_luminance(rgb: Tuple[int, int, int]) -> float: """Calculate relative luminance for WCAG contrast ratio.""" r, g, b = [x / 255.0 for x in rgb] r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4 g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4 b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4 return 0.2126 * r + 0.7152 * g + 0.0722 * b def get_contrast_ratio(color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float: """Calculate WCAG contrast ratio between two colors.""" l1 = calculate_luminance(color1) l2 = calculate_luminance(color2) lighter = max(l1, l2) darker = min(l1, l2) return (lighter + 0.05) / (darker + 0.05) def ensure_readable(fg: Tuple[int, int, int], bg: Tuple[int, int, int]) -> Tuple[int, int, int]: """Ensure foreground is readable on background (WCAG AA minimum 4.5:1).""" ratio = get_contrast_ratio(fg, bg) if ratio >= 4.5: return fg # Adjust foreground to be lighter or darker fg_lum = calculate_luminance(fg) bg_lum = calculate_luminance(bg) if bg_lum > 0.5: # Dark background → use white return (255, 255, 255) else: # Light background → use black return (0, 0, 0)