| """ |
| Token Schema Definitions |
| Design System Extractor v2 |
| |
| Pydantic models for all token types and extraction results. |
| These are the core data structures used throughout the application. |
| """ |
|
|
| from datetime import datetime |
| from enum import Enum |
| from typing import Optional, Any |
| from pydantic import BaseModel, Field, field_validator |
|
|
|
|
| |
| |
| |
|
|
| class TokenSource(str, Enum): |
| """Origin of a token value.""" |
| DETECTED = "detected" |
| INFERRED = "inferred" |
| UPGRADED = "upgraded" |
| MANUAL = "manual" |
|
|
|
|
| class Confidence(str, Enum): |
| """Confidence level for extracted tokens.""" |
| HIGH = "high" |
| MEDIUM = "medium" |
| LOW = "low" |
|
|
|
|
| class Viewport(str, Enum): |
| """Viewport type.""" |
| DESKTOP = "desktop" |
| MOBILE = "mobile" |
|
|
|
|
| class PageType(str, Enum): |
| """Type of page template.""" |
| HOMEPAGE = "homepage" |
| LISTING = "listing" |
| DETAIL = "detail" |
| FORM = "form" |
| MARKETING = "marketing" |
| AUTH = "auth" |
| CHECKOUT = "checkout" |
| ABOUT = "about" |
| CONTACT = "contact" |
| OTHER = "other" |
|
|
|
|
| |
| |
| |
|
|
| class BaseToken(BaseModel): |
| """Base class for all tokens.""" |
| source: TokenSource = TokenSource.DETECTED |
| confidence: Confidence = Confidence.MEDIUM |
| frequency: int = 0 |
| suggested_name: Optional[str] = None |
| |
| |
| accepted: bool = True |
| flagged: bool = False |
| notes: Optional[str] = None |
|
|
|
|
| |
| |
| |
|
|
| class ColorToken(BaseToken): |
| """Extracted color token.""" |
| value: str |
| value_rgb: Optional[str] = None |
| value_hsl: Optional[str] = None |
| |
| |
| contexts: list[str] = Field(default_factory=list) |
| elements: list[str] = Field(default_factory=list) |
| css_properties: list[str] = Field(default_factory=list) |
| |
| |
| contrast_white: Optional[float] = None |
| contrast_black: Optional[float] = None |
| wcag_aa_large_text: bool = False |
| wcag_aa_small_text: bool = False |
| wcag_aaa_large_text: bool = False |
| wcag_aaa_small_text: bool = False |
| |
| @field_validator("value") |
| @classmethod |
| def validate_hex(cls, v: str) -> str: |
| """Ensure hex color is properly formatted.""" |
| v = v.strip().lower() |
| if not v.startswith("#"): |
| v = f"#{v}" |
| |
| if len(v) == 4: |
| v = f"#{v[1]}{v[1]}{v[2]}{v[2]}{v[3]}{v[3]}" |
| return v |
|
|
|
|
| class ColorRamp(BaseModel): |
| """Generated color ramp with shades.""" |
| base_color: str |
| name: str |
| shades: dict[str, str] = Field(default_factory=dict) |
| source: TokenSource = TokenSource.UPGRADED |
|
|
|
|
| |
| |
| |
|
|
| class TypographyToken(BaseToken): |
| """Extracted typography token.""" |
| font_family: str |
| font_size: str |
| font_size_px: Optional[float] = None |
| font_weight: int = 400 |
| line_height: str = "1.5" |
| line_height_computed: Optional[float] = None |
| letter_spacing: Optional[str] = None |
| text_transform: Optional[str] = None |
| |
| |
| elements: list[str] = Field(default_factory=list) |
| css_selectors: list[str] = Field(default_factory=list) |
|
|
|
|
| class TypeScale(BaseModel): |
| """Typography scale configuration.""" |
| name: str |
| ratio: float |
| base_size: int = 16 |
| sizes: dict[str, str] = Field(default_factory=dict) |
| source: TokenSource = TokenSource.UPGRADED |
|
|
|
|
| class FontFamily(BaseModel): |
| """Font family information.""" |
| name: str |
| fallbacks: list[str] = Field(default_factory=list) |
| category: str = "sans-serif" |
| frequency: int = 0 |
| usage: str = "primary" |
|
|
|
|
| |
| |
| |
|
|
| class SpacingToken(BaseToken): |
| """Extracted spacing token.""" |
| value: str |
| value_px: int |
| |
| |
| contexts: list[str] = Field(default_factory=list) |
| properties: list[str] = Field(default_factory=list) |
| |
| |
| fits_base_4: bool = False |
| fits_base_8: bool = False |
| is_outlier: bool = False |
|
|
|
|
| class SpacingScale(BaseModel): |
| """Spacing scale configuration.""" |
| name: str |
| base: int |
| scale: list[int] = Field(default_factory=list) |
| names: dict[int, str] = Field(default_factory=dict) |
| source: TokenSource = TokenSource.UPGRADED |
|
|
|
|
| |
| |
| |
|
|
| class RadiusToken(BaseToken): |
| """Extracted border radius token.""" |
| value: str |
| value_px: Optional[int] = None |
| |
| |
| elements: list[str] = Field(default_factory=list) |
| |
| |
| fits_base_4: bool = False |
| fits_base_8: bool = False |
|
|
|
|
| |
| |
| |
|
|
| class ShadowToken(BaseToken): |
| """Extracted box shadow token.""" |
| value: str |
| |
| |
| offset_x: Optional[str] = None |
| offset_y: Optional[str] = None |
| blur: Optional[str] = None |
| spread: Optional[str] = None |
| color: Optional[str] = None |
| inset: bool = False |
| |
| |
| elements: list[str] = Field(default_factory=list) |
|
|
|
|
| |
| |
| |
|
|
| class DiscoveredPage(BaseModel): |
| """A page discovered during crawling.""" |
| url: str |
| title: Optional[str] = None |
| page_type: PageType = PageType.OTHER |
| depth: int = 0 |
| selected: bool = True |
| |
| |
| crawled: bool = False |
| error: Optional[str] = None |
|
|
|
|
| class CrawlResult(BaseModel): |
| """Result of crawling a single page.""" |
| url: str |
| viewport: Viewport |
| success: bool |
| |
| |
| started_at: datetime |
| completed_at: Optional[datetime] = None |
| duration_ms: Optional[int] = None |
| |
| |
| colors_found: int = 0 |
| typography_found: int = 0 |
| spacing_found: int = 0 |
| |
| |
| error: Optional[str] = None |
| warnings: list[str] = Field(default_factory=list) |
|
|
|
|
| |
| |
| |
|
|
| class ExtractedTokens(BaseModel): |
| """Complete extraction result for one viewport.""" |
| viewport: Viewport |
| source_url: str |
| pages_crawled: list[str] = Field(default_factory=list) |
| |
| |
| colors: list[ColorToken] = Field(default_factory=list) |
| typography: list[TypographyToken] = Field(default_factory=list) |
| spacing: list[SpacingToken] = Field(default_factory=list) |
| radius: list[RadiusToken] = Field(default_factory=list) |
| shadows: list[ShadowToken] = Field(default_factory=list) |
| |
| |
| font_families: list[FontFamily] = Field(default_factory=list) |
| base_font_size: Optional[str] = None |
| spacing_base: Optional[int] = None |
| naming_convention: Optional[str] = None |
| |
| |
| extraction_timestamp: datetime = Field(default_factory=datetime.now) |
| extraction_duration_ms: Optional[int] = None |
| |
| |
| total_elements_analyzed: int = 0 |
| unique_colors: int = 0 |
| unique_font_sizes: int = 0 |
| unique_spacing_values: int = 0 |
| |
| |
| errors: list[str] = Field(default_factory=list) |
| warnings: list[str] = Field(default_factory=list) |
| |
| def summary(self) -> dict: |
| """Get extraction summary.""" |
| return { |
| "viewport": self.viewport.value, |
| "pages_crawled": len(self.pages_crawled), |
| "colors": len(self.colors), |
| "typography": len(self.typography), |
| "spacing": len(self.spacing), |
| "radius": len(self.radius), |
| "shadows": len(self.shadows), |
| "font_families": len(self.font_families), |
| "errors": len(self.errors), |
| "warnings": len(self.warnings), |
| } |
|
|
|
|
| |
| |
| |
|
|
| class NormalizedTokens(BaseModel): |
| """Normalized and structured tokens from Agent 2.""" |
| viewport: Viewport |
| source_url: str |
| |
| |
| colors: dict[str, ColorToken] = Field(default_factory=dict) |
| typography: dict[str, TypographyToken] = Field(default_factory=dict) |
| spacing: dict[str, SpacingToken] = Field(default_factory=dict) |
| radius: dict[str, RadiusToken] = Field(default_factory=dict) |
| shadows: dict[str, ShadowToken] = Field(default_factory=dict) |
| |
| |
| font_families: list[FontFamily] = Field(default_factory=list) |
| detected_spacing_base: Optional[int] = None |
| detected_naming_convention: Optional[str] = None |
| |
| |
| duplicate_colors: list[tuple[str, str]] = Field(default_factory=list) |
| conflicting_tokens: list[str] = Field(default_factory=list) |
| |
| |
| normalized_at: datetime = Field(default_factory=datetime.now) |
|
|
|
|
| |
| |
| |
|
|
| class UpgradeOption(BaseModel): |
| """A single upgrade option.""" |
| id: str |
| name: str |
| description: str |
| category: str |
| |
| |
| values: dict[str, Any] = Field(default_factory=dict) |
| |
| |
| pros: list[str] = Field(default_factory=list) |
| cons: list[str] = Field(default_factory=list) |
| effort: str = "low" |
| recommended: bool = False |
| |
| |
| selected: bool = False |
|
|
|
|
| class UpgradeRecommendations(BaseModel): |
| """All upgrade recommendations from Agent 3.""" |
| |
| |
| typography_scales: list[UpgradeOption] = Field(default_factory=list) |
| spacing_systems: list[UpgradeOption] = Field(default_factory=list) |
| color_ramps: list[UpgradeOption] = Field(default_factory=list) |
| naming_conventions: list[UpgradeOption] = Field(default_factory=list) |
| |
| |
| llm_rationale: str = "" |
| detected_patterns: list[str] = Field(default_factory=list) |
| brand_analysis: list[dict] = Field(default_factory=list) |
| color_observations: str = "" |
| |
| |
| accessibility_issues: list[str] = Field(default_factory=list) |
| accessibility_fixes: list[UpgradeOption] = Field(default_factory=list) |
| |
| |
| generated_at: datetime = Field(default_factory=datetime.now) |
|
|
|
|
| |
| |
| |
|
|
| class TokenMetadata(BaseModel): |
| """Metadata for exported tokens.""" |
| source_url: str |
| extracted_at: datetime |
| version: str |
| viewport: Viewport |
| generator: str = "Design System Extractor v2" |
|
|
|
|
| class FinalTokens(BaseModel): |
| """Final exported token set.""" |
| metadata: TokenMetadata |
| |
| |
| colors: dict[str, dict] = Field(default_factory=dict) |
| typography: dict[str, dict] = Field(default_factory=dict) |
| spacing: dict[str, dict] = Field(default_factory=dict) |
| radius: dict[str, dict] = Field(default_factory=dict) |
| shadows: dict[str, dict] = Field(default_factory=dict) |
| |
| def to_tokens_studio_format(self) -> dict: |
| """Convert to Tokens Studio compatible format.""" |
| return { |
| "$metadata": { |
| "source": self.metadata.source_url, |
| "version": self.metadata.version, |
| }, |
| "color": self.colors, |
| "typography": self.typography, |
| "spacing": self.spacing, |
| "borderRadius": self.radius, |
| "boxShadow": self.shadows, |
| } |
| |
| def to_css_variables(self) -> str: |
| """Convert to CSS custom properties.""" |
| lines = [":root {"] |
| |
| for name, data in self.colors.items(): |
| value = data.get("value", data) if isinstance(data, dict) else data |
| lines.append(f" --color-{name}: {value};") |
| |
| for name, data in self.spacing.items(): |
| value = data.get("value", data) if isinstance(data, dict) else data |
| lines.append(f" --space-{name}: {value};") |
| |
| lines.append("}") |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| class WorkflowState(BaseModel): |
| """LangGraph workflow state.""" |
| |
| |
| base_url: str |
| |
| |
| discovered_pages: list[DiscoveredPage] = Field(default_factory=list) |
| confirmed_pages: list[str] = Field(default_factory=list) |
| |
| |
| desktop_tokens: Optional[ExtractedTokens] = None |
| mobile_tokens: Optional[ExtractedTokens] = None |
| |
| |
| desktop_normalized: Optional[NormalizedTokens] = None |
| mobile_normalized: Optional[NormalizedTokens] = None |
| |
| |
| upgrade_recommendations: Optional[UpgradeRecommendations] = None |
| selected_upgrades: dict[str, str] = Field(default_factory=dict) |
| |
| |
| desktop_final: Optional[FinalTokens] = None |
| mobile_final: Optional[FinalTokens] = None |
| |
| |
| current_stage: str = "init" |
| errors: list[str] = Field(default_factory=list) |
| warnings: list[str] = Field(default_factory=list) |
| |
| |
| started_at: Optional[datetime] = None |
| completed_at: Optional[datetime] = None |
| |
| class Config: |
| arbitrary_types_allowed = True |
|
|