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