Spaces:
Sleeping
Sleeping
| """ | |
| Preview Generator for Typography and Color Previews | |
| Generates HTML previews for: | |
| 1. Typography - Actual font rendering with detected styles | |
| 2. Colors AS-IS - Simple swatches showing extracted colors (Stage 1) | |
| 3. Color Ramps - 11 shades (50-950) with AA compliance (Stage 2) | |
| 4. Spacing AS-IS - Visual spacing blocks | |
| 5. Radius AS-IS - Rounded corner examples | |
| 6. Shadows AS-IS - Shadow examples | |
| """ | |
| from typing import Optional | |
| import colorsys | |
| import re | |
| # ============================================================================= | |
| # STAGE 1: AS-IS PREVIEWS (No enhancements, just raw extracted values) | |
| # ============================================================================= | |
| def generate_colors_asis_preview_html( | |
| color_tokens: dict, | |
| background: str = "#FAFAFA" | |
| ) -> str: | |
| """ | |
| Generate HTML preview for AS-IS colors (Stage 1). | |
| Shows simple color swatches without generated ramps. | |
| Args: | |
| color_tokens: Dict of colors {name: {value: "#hex", ...}} | |
| background: Background color | |
| Returns: | |
| HTML string for Gradio HTML component | |
| """ | |
| rows_html = "" | |
| for name, token in list(color_tokens.items())[:20]: # Limit to 20 colors | |
| # Get hex value | |
| if isinstance(token, dict): | |
| hex_val = token.get("value", "#888888") | |
| frequency = token.get("frequency", 0) | |
| contexts = token.get("contexts", []) | |
| contrast_white = token.get("contrast_white", 0) | |
| else: | |
| hex_val = str(token) | |
| frequency = 0 | |
| contexts = [] | |
| contrast_white = 0 | |
| # Clean up hex | |
| if not hex_val.startswith("#"): | |
| hex_val = f"#{hex_val}" | |
| # Clean name | |
| display_name = name.replace("_", " ").replace("-", " ").title() | |
| if len(display_name) > 20: | |
| display_name = display_name[:17] + "..." | |
| # AA compliance check | |
| aa_status = "✓ AA" if contrast_white and contrast_white >= 4.5 else "✗ AA" if contrast_white else "" | |
| aa_class = "aa-pass" if contrast_white and contrast_white >= 4.5 else "aa-fail" | |
| # Context badges | |
| context_html = "" | |
| for ctx in contexts[:2]: | |
| context_html += f'<span class="context-badge">{ctx}</span>' | |
| rows_html += f''' | |
| <div class="color-row-asis"> | |
| <div class="color-swatch-large" style="background-color: {hex_val};"></div> | |
| <div class="color-info-asis"> | |
| <div class="color-name-asis">{display_name}</div> | |
| <div class="color-hex-asis">{hex_val}</div> | |
| <div class="color-meta-asis"> | |
| <span class="frequency">Used {frequency}x</span> | |
| {context_html} | |
| <span class="{aa_class}">{aa_status}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ''' | |
| html = f''' | |
| <style> | |
| .colors-asis-preview {{ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: {background}; | |
| border-radius: 12px; | |
| padding: 20px; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 16px; | |
| }} | |
| .color-row-asis {{ | |
| display: flex; | |
| align-items: center; | |
| background: #fff; | |
| border-radius: 8px; | |
| padding: 12px; | |
| border: 1px solid #e0e0e0; | |
| }} | |
| .color-swatch-large {{ | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 8px; | |
| border: 2px solid rgba(0,0,0,0.1); | |
| margin-right: 16px; | |
| flex-shrink: 0; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| }} | |
| .color-info-asis {{ | |
| flex: 1; | |
| }} | |
| .color-name-asis {{ | |
| font-weight: 700; | |
| font-size: 14px; | |
| color: #1a1a1a; | |
| margin-bottom: 4px; | |
| }} | |
| .color-hex-asis {{ | |
| font-size: 13px; | |
| color: #444; | |
| font-family: 'SF Mono', Monaco, monospace; | |
| margin-bottom: 6px; | |
| }} | |
| .color-meta-asis {{ | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| }} | |
| .frequency {{ | |
| font-size: 11px; | |
| color: #666; | |
| }} | |
| .context-badge {{ | |
| font-size: 10px; | |
| background: #e8e8e8; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| color: #555; | |
| }} | |
| .aa-pass {{ | |
| font-size: 11px; | |
| color: #16a34a; | |
| font-weight: 600; | |
| }} | |
| .aa-fail {{ | |
| font-size: 11px; | |
| color: #dc2626; | |
| font-weight: 600; | |
| }} | |
| </style> | |
| <div class="colors-asis-preview"> | |
| {rows_html} | |
| </div> | |
| ''' | |
| return html | |
| def generate_spacing_asis_preview_html( | |
| spacing_tokens: dict, | |
| background: str = "#FAFAFA" | |
| ) -> str: | |
| """ | |
| Generate HTML preview for AS-IS spacing (Stage 1). | |
| Shows visual blocks representing each spacing value. | |
| """ | |
| rows_html = "" | |
| # Sort by pixel value | |
| sorted_tokens = [] | |
| for name, token in spacing_tokens.items(): | |
| if isinstance(token, dict): | |
| value_px = token.get("value_px", 0) | |
| value = token.get("value", "0px") | |
| else: | |
| value = str(token) | |
| value_px = float(re.sub(r'[^0-9.]', '', value) or 0) | |
| sorted_tokens.append((name, token, value_px, value)) | |
| sorted_tokens.sort(key=lambda x: x[2]) | |
| for name, token, value_px, value in sorted_tokens[:15]: | |
| # Cap visual width at 200px | |
| visual_width = min(value_px, 200) | |
| rows_html += f''' | |
| <div class="spacing-row-asis"> | |
| <div class="spacing-label">{value}</div> | |
| <div class="spacing-bar" style="width: {visual_width}px;"></div> | |
| </div> | |
| ''' | |
| html = f''' | |
| <style> | |
| .spacing-asis-preview {{ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: {background}; | |
| border-radius: 12px; | |
| padding: 20px; | |
| }} | |
| .spacing-row-asis {{ | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| }} | |
| .spacing-label {{ | |
| width: 60px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #333; | |
| font-family: monospace; | |
| }} | |
| .spacing-bar {{ | |
| height: 24px; | |
| background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%); | |
| border-radius: 4px; | |
| min-width: 4px; | |
| }} | |
| </style> | |
| <div class="spacing-asis-preview"> | |
| {rows_html} | |
| </div> | |
| ''' | |
| return html | |
| def generate_radius_asis_preview_html( | |
| radius_tokens: dict, | |
| background: str = "#FAFAFA" | |
| ) -> str: | |
| """ | |
| Generate HTML preview for AS-IS border radius (Stage 1). | |
| Shows boxes with each radius value applied. | |
| """ | |
| rows_html = "" | |
| for name, token in list(radius_tokens.items())[:12]: | |
| if isinstance(token, dict): | |
| value = token.get("value", "0px") | |
| else: | |
| value = str(token) | |
| rows_html += f''' | |
| <div class="radius-item"> | |
| <div class="radius-box" style="border-radius: {value};"></div> | |
| <div class="radius-label">{value}</div> | |
| </div> | |
| ''' | |
| html = f''' | |
| <style> | |
| .radius-asis-preview {{ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: {background}; | |
| border-radius: 12px; | |
| padding: 20px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 16px; | |
| }} | |
| .radius-item {{ | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| }} | |
| .radius-box {{ | |
| width: 60px; | |
| height: 60px; | |
| background: #3b82f6; | |
| margin-bottom: 8px; | |
| }} | |
| .radius-label {{ | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #333; | |
| font-family: monospace; | |
| }} | |
| </style> | |
| <div class="radius-asis-preview"> | |
| {rows_html} | |
| </div> | |
| ''' | |
| return html | |
| def generate_shadows_asis_preview_html( | |
| shadow_tokens: dict, | |
| background: str = "#FAFAFA" | |
| ) -> str: | |
| """ | |
| Generate HTML preview for AS-IS shadows (Stage 1). | |
| Shows cards with each shadow value applied. | |
| """ | |
| rows_html = "" | |
| for name, token in list(shadow_tokens.items())[:8]: | |
| if isinstance(token, dict): | |
| value = token.get("value", "none") | |
| else: | |
| value = str(token) | |
| # Clean name for display | |
| display_name = name.replace("_", " ").replace("-", " ").title() | |
| if len(display_name) > 15: | |
| display_name = display_name[:12] + "..." | |
| rows_html += f''' | |
| <div class="shadow-item"> | |
| <div class="shadow-box" style="box-shadow: {value};"></div> | |
| <div class="shadow-label">{display_name}</div> | |
| <div class="shadow-value">{value[:40]}...</div> | |
| </div> | |
| ''' | |
| html = f''' | |
| <style> | |
| .shadows-asis-preview {{ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: {background}; | |
| border-radius: 12px; | |
| padding: 20px; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | |
| gap: 24px; | |
| }} | |
| .shadow-item {{ | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| }} | |
| .shadow-box {{ | |
| width: 100px; | |
| height: 100px; | |
| background: #fff; | |
| border-radius: 8px; | |
| margin-bottom: 12px; | |
| }} | |
| .shadow-label {{ | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 4px; | |
| }} | |
| .shadow-value {{ | |
| font-size: 10px; | |
| color: #666; | |
| font-family: monospace; | |
| text-align: center; | |
| word-break: break-all; | |
| }} | |
| </style> | |
| <div class="shadows-asis-preview"> | |
| {rows_html} | |
| </div> | |
| ''' | |
| return html | |
| # ============================================================================= | |
| # STAGE 2: TYPOGRAPHY PREVIEW (with rendered font) | |
| # ============================================================================= | |
| def generate_typography_preview_html( | |
| typography_tokens: dict, | |
| font_family: str = "Open Sans", | |
| background: str = "#FAFAFA", | |
| sample_text: str = "The quick brown fox jumps over the lazy dog" | |
| ) -> str: | |
| """ | |
| Generate HTML preview for typography tokens. | |
| Args: | |
| typography_tokens: Dict of typography styles {name: {font_size, font_weight, line_height, letter_spacing}} | |
| font_family: Primary font family detected | |
| background: Background color (neutral) | |
| sample_text: Text to render for preview | |
| Returns: | |
| HTML string for Gradio HTML component | |
| """ | |
| # Sort tokens by font size (largest first) | |
| sorted_tokens = [] | |
| for name, token in typography_tokens.items(): | |
| size_str = str(token.get("font_size", "16px")) | |
| size_num = float(re.sub(r'[^0-9.]', '', size_str) or 16) | |
| sorted_tokens.append((name, token, size_num)) | |
| sorted_tokens.sort(key=lambda x: -x[2]) # Descending by size | |
| # Generate rows | |
| rows_html = "" | |
| for name, token, size_num in sorted_tokens[:15]: # Limit to 15 styles | |
| font_size = token.get("font_size", "16px") | |
| font_weight = token.get("font_weight", "400") | |
| line_height = token.get("line_height", "1.5") | |
| letter_spacing = token.get("letter_spacing", "0") | |
| # Convert weight names to numbers | |
| weight_map = { | |
| "thin": 100, "extralight": 200, "light": 300, "regular": 400, | |
| "medium": 500, "semibold": 600, "bold": 700, "extrabold": 800, "black": 900 | |
| } | |
| if isinstance(font_weight, str) and font_weight.lower() in weight_map: | |
| font_weight = weight_map[font_weight.lower()] | |
| # Weight label | |
| weight_labels = { | |
| 100: "Thin", 200: "ExtraLight", 300: "Light", 400: "Regular", | |
| 500: "Medium", 600: "SemiBold", 700: "Bold", 800: "ExtraBold", 900: "Black" | |
| } | |
| weight_label = weight_labels.get(int(font_weight) if str(font_weight).isdigit() else 400, "Regular") | |
| # Clean up name for display | |
| display_name = name.replace("_", " ").replace("-", " ").title() | |
| if len(display_name) > 15: | |
| display_name = display_name[:15] + "..." | |
| # Truncate sample text for large sizes | |
| display_text = sample_text | |
| if size_num > 48: | |
| display_text = sample_text[:30] + "..." | |
| elif size_num > 32: | |
| display_text = sample_text[:40] + "..." | |
| rows_html += f''' | |
| <tr class="meta-row"> | |
| <td class="scale-name"> | |
| <div class="scale-label">{display_name}</div> | |
| </td> | |
| <td class="meta">{font_family}</td> | |
| <td class="meta">{weight_label}</td> | |
| <td class="meta">{int(size_num)}</td> | |
| <td class="meta">Sentence</td> | |
| <td class="meta">{letter_spacing}</td> | |
| </tr> | |
| <tr> | |
| <td colspan="6" class="preview-cell"> | |
| <div class="preview-text" style=" | |
| font-family: '{font_family}', sans-serif; | |
| font-size: {font_size}; | |
| font-weight: {font_weight}; | |
| line-height: {line_height}; | |
| letter-spacing: {letter_spacing}px; | |
| ">{display_text}</div> | |
| </td> | |
| </tr> | |
| ''' | |
| html = f''' | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family={font_family.replace(" ", "+")}:wght@100;200;300;400;500;600;700;800;900&display=swap'); | |
| .typography-preview {{ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: {background}; | |
| border-radius: 12px; | |
| padding: 20px; | |
| overflow-x: auto; | |
| }} | |
| .typography-preview table {{ | |
| width: 100%; | |
| border-collapse: collapse; | |
| }} | |
| .typography-preview th {{ | |
| text-align: left; | |
| padding: 12px 16px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #333; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| border-bottom: 2px solid #E0E0E0; | |
| background: #F5F5F5; | |
| }} | |
| .typography-preview td {{ | |
| padding: 8px 16px; | |
| vertical-align: middle; | |
| }} | |
| .typography-preview .meta-row {{ | |
| background: #F8F8F8; | |
| border-top: 1px solid #E8E8E8; | |
| }} | |
| .typography-preview .scale-name {{ | |
| font-weight: 700; | |
| color: #1A1A1A; | |
| min-width: 120px; | |
| }} | |
| .typography-preview .scale-label {{ | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #1A1A1A; | |
| background: #E8E8E8; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| display: inline-block; | |
| }} | |
| .typography-preview .meta {{ | |
| font-size: 13px; | |
| color: #444; | |
| white-space: nowrap; | |
| }} | |
| .typography-preview .preview-cell {{ | |
| padding: 16px; | |
| background: #FFFFFF; | |
| border-bottom: 1px solid #E8E8E8; | |
| }} | |
| .typography-preview .preview-text {{ | |
| color: #1A1A1A; | |
| margin: 0; | |
| word-break: break-word; | |
| }} | |
| .typography-preview tr:hover .preview-cell {{ | |
| background: #F5F5F5; | |
| }} | |
| </style> | |
| <div class="typography-preview"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Scale Category</th> | |
| <th>Typeface</th> | |
| <th>Weight</th> | |
| <th>Size</th> | |
| <th>Case</th> | |
| <th>Letter Spacing</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {rows_html} | |
| </tbody> | |
| </table> | |
| </div> | |
| ''' | |
| return html | |
| # ============================================================================= | |
| # COLOR RAMP PREVIEW | |
| # ============================================================================= | |
| def hex_to_rgb(hex_color: str) -> tuple: | |
| """Convert hex color to RGB tuple.""" | |
| hex_color = hex_color.lstrip('#') | |
| if len(hex_color) == 3: | |
| hex_color = ''.join([c*2 for c in hex_color]) | |
| return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) | |
| def rgb_to_hex(rgb: tuple) -> str: | |
| """Convert RGB tuple to hex string.""" | |
| return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2])) | |
| def get_luminance(rgb: tuple) -> float: | |
| """Calculate relative luminance for contrast ratio.""" | |
| def adjust(c): | |
| c = c / 255 | |
| return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 | |
| r, g, b = rgb | |
| return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b) | |
| def get_contrast_ratio(color1: tuple, color2: tuple) -> float: | |
| """Calculate contrast ratio between two colors.""" | |
| l1 = get_luminance(color1) | |
| l2 = get_luminance(color2) | |
| lighter = max(l1, l2) | |
| darker = min(l1, l2) | |
| return (lighter + 0.05) / (darker + 0.05) | |
| def generate_color_ramp(base_hex: str) -> list[dict]: | |
| """ | |
| Generate 11 shades (50-950) from a base color. | |
| Uses OKLCH-like approach for perceptually uniform steps. | |
| """ | |
| try: | |
| rgb = hex_to_rgb(base_hex) | |
| except: | |
| return [] | |
| # Convert to HLS for easier manipulation | |
| r, g, b = [x / 255 for x in rgb] | |
| h, l, s = colorsys.rgb_to_hls(r, g, b) | |
| # Define lightness levels for each shade | |
| # 50 = very light (0.95), 500 = base, 950 = very dark (0.05) | |
| shade_lightness = { | |
| 50: 0.95, | |
| 100: 0.90, | |
| 200: 0.80, | |
| 300: 0.70, | |
| 400: 0.60, | |
| 500: l, # Keep original lightness for 500 | |
| 600: 0.45, | |
| 700: 0.35, | |
| 800: 0.25, | |
| 900: 0.15, | |
| 950: 0.08, | |
| } | |
| # Adjust saturation for light/dark shades | |
| ramp = [] | |
| for shade, target_l in shade_lightness.items(): | |
| # Reduce saturation for very light colors | |
| if target_l > 0.8: | |
| adjusted_s = s * 0.6 | |
| elif target_l < 0.2: | |
| adjusted_s = s * 0.8 | |
| else: | |
| adjusted_s = s | |
| # Generate new RGB | |
| new_r, new_g, new_b = colorsys.hls_to_rgb(h, target_l, adjusted_s) | |
| new_rgb = (int(new_r * 255), int(new_g * 255), int(new_b * 255)) | |
| new_hex = rgb_to_hex(new_rgb) | |
| # Check AA compliance | |
| white = (255, 255, 255) | |
| black = (0, 0, 0) | |
| contrast_white = get_contrast_ratio(new_rgb, white) | |
| contrast_black = get_contrast_ratio(new_rgb, black) | |
| # AA requires 4.5:1 for normal text | |
| aa_on_white = contrast_white >= 4.5 | |
| aa_on_black = contrast_black >= 4.5 | |
| ramp.append({ | |
| "shade": shade, | |
| "hex": new_hex, | |
| "rgb": new_rgb, | |
| "contrast_white": round(contrast_white, 2), | |
| "contrast_black": round(contrast_black, 2), | |
| "aa_on_white": aa_on_white, | |
| "aa_on_black": aa_on_black, | |
| }) | |
| return ramp | |
| def generate_color_ramps_preview_html( | |
| color_tokens: dict, | |
| background: str = "#FAFAFA" | |
| ) -> str: | |
| """ | |
| Generate HTML preview for color ramps. | |
| Args: | |
| color_tokens: Dict of colors {name: {value: "#hex", ...}} | |
| background: Background color | |
| Returns: | |
| HTML string for Gradio HTML component | |
| """ | |
| rows_html = "" | |
| for name, token in list(color_tokens.items())[:10]: # Limit to 10 colors | |
| # Get hex value | |
| if isinstance(token, dict): | |
| hex_val = token.get("value", "#888888") | |
| else: | |
| hex_val = str(token) | |
| # Clean up hex | |
| if not hex_val.startswith("#"): | |
| hex_val = f"#{hex_val}" | |
| # Generate ramp | |
| ramp = generate_color_ramp(hex_val) | |
| if not ramp: | |
| continue | |
| # Clean name | |
| display_name = name.replace("_", " ").replace("-", " ").title() | |
| if len(display_name) > 15: | |
| display_name = display_name[:12] + "..." | |
| # Generate shade cells | |
| shades_html = "" | |
| for shade_info in ramp: | |
| shade = shade_info["shade"] | |
| hex_color = shade_info["hex"] | |
| aa_white = shade_info["aa_on_white"] | |
| aa_black = shade_info["aa_on_black"] | |
| # Determine text color for label | |
| text_color = "#000" if shade < 500 else "#FFF" | |
| # AA indicator | |
| if aa_white or aa_black: | |
| aa_indicator = "✓" | |
| aa_class = "aa-pass" | |
| else: | |
| aa_indicator = "✗" | |
| aa_class = "aa-fail" | |
| shades_html += f''' | |
| <div class="shade-cell" style="background-color: {hex_color};" title="{hex_color} | AA: {'Pass' if aa_white or aa_black else 'Fail'}"> | |
| <span class="shade-label" style="color: {text_color};">{shade}</span> | |
| <span class="aa-badge {aa_class}">{aa_indicator}</span> | |
| </div> | |
| ''' | |
| rows_html += f''' | |
| <div class="color-row"> | |
| <div class="color-info"> | |
| <div class="color-swatch" style="background-color: {hex_val};"></div> | |
| <div class="color-meta"> | |
| <div class="color-name">{display_name}</div> | |
| <div class="color-hex">{hex_val}</div> | |
| </div> | |
| </div> | |
| <div class="color-ramp"> | |
| {shades_html} | |
| </div> | |
| </div> | |
| ''' | |
| html = f''' | |
| <style> | |
| .color-ramps-preview {{ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: {background}; | |
| border-radius: 12px; | |
| padding: 20px; | |
| overflow-x: auto; | |
| }} | |
| .color-row {{ | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| padding-bottom: 16px; | |
| border-bottom: 1px solid #E8E8E8; | |
| }} | |
| .color-row:last-child {{ | |
| border-bottom: none; | |
| margin-bottom: 0; | |
| padding-bottom: 0; | |
| }} | |
| .color-info {{ | |
| display: flex; | |
| align-items: center; | |
| min-width: 140px; | |
| margin-right: 20px; | |
| }} | |
| .color-swatch {{ | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| border: 2px solid rgba(0,0,0,0.15); | |
| margin-right: 12px; | |
| flex-shrink: 0; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| }} | |
| .color-meta {{ | |
| flex: 1; | |
| min-width: 100px; | |
| }} | |
| .color-name {{ | |
| font-weight: 700; | |
| font-size: 13px; | |
| color: #1A1A1A; | |
| margin-bottom: 2px; | |
| background: #E8E8E8; | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| display: inline-block; | |
| }} | |
| .color-hex {{ | |
| font-size: 11px; | |
| color: #444; | |
| font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; | |
| margin-top: 4px; | |
| }} | |
| .color-ramp {{ | |
| display: flex; | |
| gap: 4px; | |
| flex: 1; | |
| }} | |
| .shade-cell {{ | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 6px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| cursor: pointer; | |
| transition: transform 0.15s; | |
| border: 1px solid rgba(0,0,0,0.1); | |
| }} | |
| .shade-cell:hover {{ | |
| transform: scale(1.1); | |
| z-index: 10; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| }} | |
| .shade-label {{ | |
| font-size: 10px; | |
| font-weight: 700; | |
| }} | |
| .aa-badge {{ | |
| font-size: 12px; | |
| margin-top: 2px; | |
| font-weight: 700; | |
| }} | |
| .aa-pass {{ | |
| color: #16A34A; | |
| }} | |
| .aa-fail {{ | |
| color: #DC2626; | |
| }} | |
| .shade-cell:hover .shade-label, | |
| .shade-cell:hover .aa-badge {{ | |
| opacity: 1; | |
| }} | |
| /* Header row */ | |
| .ramp-header {{ | |
| display: flex; | |
| margin-bottom: 12px; | |
| padding-left: 160px; | |
| }} | |
| .ramp-header-label {{ | |
| width: 48px; | |
| text-align: center; | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: #666; | |
| margin-right: 4px; | |
| }} | |
| </style> | |
| <div class="color-ramps-preview"> | |
| <div class="ramp-header"> | |
| <span class="ramp-header-label">50</span> | |
| <span class="ramp-header-label">100</span> | |
| <span class="ramp-header-label">200</span> | |
| <span class="ramp-header-label">300</span> | |
| <span class="ramp-header-label">400</span> | |
| <span class="ramp-header-label">500</span> | |
| <span class="ramp-header-label">600</span> | |
| <span class="ramp-header-label">700</span> | |
| <span class="ramp-header-label">800</span> | |
| <span class="ramp-header-label">900</span> | |
| <span class="ramp-header-label">950</span> | |
| </div> | |
| {rows_html} | |
| </div> | |
| ''' | |
| return html | |
| # ============================================================================= | |
| # COMBINED PREVIEW | |
| # ============================================================================= | |
| def generate_design_system_preview_html( | |
| typography_tokens: dict, | |
| color_tokens: dict, | |
| font_family: str = "Open Sans", | |
| sample_text: str = "The quick brown fox jumps over the lazy dog" | |
| ) -> tuple[str, str]: | |
| """ | |
| Generate both typography and color ramp previews. | |
| Returns: | |
| Tuple of (typography_html, color_ramps_html) | |
| """ | |
| typography_html = generate_typography_preview_html( | |
| typography_tokens=typography_tokens, | |
| font_family=font_family, | |
| sample_text=sample_text, | |
| ) | |
| color_ramps_html = generate_color_ramps_preview_html( | |
| color_tokens=color_tokens, | |
| ) | |
| return typography_html, color_ramps_html | |