| | """ |
| | 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 |
| |
|