|
|
""" |
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_colors_asis_preview_html( |
|
|
color_tokens: dict, |
|
|
background: str = "#FAFAFA", |
|
|
max_colors: int = 50 |
|
|
) -> str: |
|
|
""" |
|
|
Generate HTML preview for AS-IS colors (Stage 1). |
|
|
|
|
|
Shows simple color swatches without generated ramps. |
|
|
Sorted by frequency (most used first). |
|
|
|
|
|
Args: |
|
|
color_tokens: Dict of colors {name: {value: "#hex", ...}} |
|
|
background: Background color |
|
|
max_colors: Maximum colors to display (default 50) |
|
|
|
|
|
Returns: |
|
|
HTML string for Gradio HTML component |
|
|
""" |
|
|
|
|
|
|
|
|
sorted_tokens = [] |
|
|
for name, token in color_tokens.items(): |
|
|
if isinstance(token, dict): |
|
|
freq = token.get("frequency", 0) |
|
|
else: |
|
|
freq = 0 |
|
|
sorted_tokens.append((name, token, freq)) |
|
|
|
|
|
sorted_tokens.sort(key=lambda x: -x[2]) |
|
|
|
|
|
rows_html = "" |
|
|
|
|
|
for name, token, freq in sorted_tokens[:max_colors]: |
|
|
|
|
|
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) |
|
|
contrast_black = token.get("contrast_black", 0) |
|
|
else: |
|
|
hex_val = str(token) |
|
|
frequency = 0 |
|
|
contexts = [] |
|
|
contrast_white = 0 |
|
|
contrast_black = 0 |
|
|
|
|
|
|
|
|
if not hex_val.startswith("#"): |
|
|
hex_val = f"#{hex_val}" |
|
|
|
|
|
|
|
|
|
|
|
text_color = "#1a1a1a" if contrast_white and contrast_white < 4.5 else "#ffffff" |
|
|
if not contrast_white: |
|
|
|
|
|
try: |
|
|
r = int(hex_val[1:3], 16) |
|
|
g = int(hex_val[3:5], 16) |
|
|
b = int(hex_val[5:7], 16) |
|
|
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 |
|
|
text_color = "#1a1a1a" if luminance > 0.5 else "#ffffff" |
|
|
except: |
|
|
text_color = "#1a1a1a" |
|
|
|
|
|
|
|
|
display_name = name.replace("_", " ").replace("-", " ").replace(".", " ").title() |
|
|
if len(display_name) > 25: |
|
|
display_name = display_name[:22] + "..." |
|
|
|
|
|
|
|
|
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_html = "" |
|
|
for ctx in contexts[:3]: |
|
|
ctx_display = ctx[:12] + "..." if len(ctx) > 12 else ctx |
|
|
context_html += f'<span class="context-badge">{ctx_display}</span>' |
|
|
|
|
|
rows_html += f''' |
|
|
<div class="color-row-asis"> |
|
|
<div class="color-swatch-large" style="background-color: {hex_val};"> |
|
|
<span class="swatch-hex" style="color: {text_color};">{hex_val}</span> |
|
|
</div> |
|
|
<div class="color-info-asis"> |
|
|
<div class="color-name-asis">{display_name}</div> |
|
|
<div class="color-meta-asis"> |
|
|
<span class="frequency">Used {frequency}x</span> |
|
|
<span class="{aa_class}">{aa_status}</span> |
|
|
</div> |
|
|
<div class="context-row"> |
|
|
{context_html} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
total_colors = len(color_tokens) |
|
|
showing = min(max_colors, total_colors) |
|
|
count_info = f"Showing {showing} of {total_colors} colors (sorted by frequency)" |
|
|
|
|
|
html = f''' |
|
|
<style> |
|
|
.colors-asis-header {{ |
|
|
font-family: system-ui, -apple-system, sans-serif; |
|
|
font-size: 14px; |
|
|
color: #333 !important; |
|
|
margin-bottom: 16px; |
|
|
padding: 8px 12px; |
|
|
background: #e8e8e8 !important; |
|
|
border-radius: 6px; |
|
|
}} |
|
|
|
|
|
.colors-asis-preview {{ |
|
|
font-family: system-ui, -apple-system, sans-serif; |
|
|
background: {background} !important; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
|
|
gap: 16px; |
|
|
max-height: 800px; |
|
|
overflow-y: auto; |
|
|
}} |
|
|
|
|
|
.color-row-asis {{ |
|
|
display: flex; |
|
|
align-items: center; |
|
|
background: #ffffff !important; |
|
|
border-radius: 8px; |
|
|
padding: 12px; |
|
|
border: 1px solid #d0d0d0 !important; |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08); |
|
|
}} |
|
|
|
|
|
.color-swatch-large {{ |
|
|
width: 80px; |
|
|
height: 80px; |
|
|
border-radius: 8px; |
|
|
border: 2px solid rgba(0,0,0,0.15) !important; |
|
|
margin-right: 16px; |
|
|
flex-shrink: 0; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
}} |
|
|
|
|
|
.swatch-hex {{ |
|
|
font-size: 11px; |
|
|
font-family: 'SF Mono', Monaco, monospace; |
|
|
font-weight: 600; |
|
|
text-shadow: 0 1px 2px rgba(0,0,0,0.4); |
|
|
}} |
|
|
|
|
|
.color-info-asis {{ |
|
|
flex: 1; |
|
|
min-width: 0; |
|
|
}} |
|
|
|
|
|
.color-name-asis {{ |
|
|
font-weight: 700; |
|
|
font-size: 14px; |
|
|
color: #1a1a1a !important; |
|
|
margin-bottom: 6px; |
|
|
white-space: nowrap; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
}} |
|
|
|
|
|
.color-meta-asis {{ |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
align-items: center; |
|
|
margin-bottom: 6px; |
|
|
}} |
|
|
|
|
|
.frequency {{ |
|
|
font-size: 12px; |
|
|
color: #333 !important; |
|
|
font-weight: 500; |
|
|
}} |
|
|
|
|
|
.context-row {{ |
|
|
display: flex; |
|
|
gap: 6px; |
|
|
flex-wrap: wrap; |
|
|
}} |
|
|
|
|
|
.context-badge {{ |
|
|
font-size: 10px; |
|
|
background: #d0d0d0 !important; |
|
|
padding: 2px 8px; |
|
|
border-radius: 4px; |
|
|
color: #222 !important; |
|
|
}} |
|
|
|
|
|
.aa-pass {{ |
|
|
font-size: 11px; |
|
|
color: #166534 !important; |
|
|
font-weight: 700; |
|
|
background: #dcfce7 !important; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
}} |
|
|
|
|
|
.aa-fail {{ |
|
|
font-size: 11px; |
|
|
color: #991b1b !important; |
|
|
font-weight: 700; |
|
|
background: #fee2e2 !important; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div class="colors-asis-header">{count_info}</div> |
|
|
<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 = "" |
|
|
|
|
|
|
|
|
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]: |
|
|
|
|
|
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: #f5f5f5 !important; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
}} |
|
|
|
|
|
.spacing-row-asis {{ |
|
|
display: flex; |
|
|
align-items: center; |
|
|
margin-bottom: 12px; |
|
|
background: #ffffff !important; |
|
|
padding: 8px 12px; |
|
|
border-radius: 6px; |
|
|
}} |
|
|
|
|
|
.spacing-label {{ |
|
|
width: 80px; |
|
|
font-size: 14px; |
|
|
font-weight: 600; |
|
|
color: #1a1a1a !important; |
|
|
font-family: 'SF Mono', Monaco, monospace; |
|
|
}} |
|
|
|
|
|
.spacing-bar {{ |
|
|
height: 24px; |
|
|
background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%) !important; |
|
|
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: #f5f5f5 !important; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 20px; |
|
|
}} |
|
|
|
|
|
.radius-item {{ |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
background: #ffffff !important; |
|
|
padding: 12px; |
|
|
border-radius: 8px; |
|
|
}} |
|
|
|
|
|
.radius-box {{ |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
background: #3b82f6 !important; |
|
|
margin-bottom: 8px; |
|
|
}} |
|
|
|
|
|
.radius-label {{ |
|
|
font-size: 13px; |
|
|
font-weight: 600; |
|
|
color: #1a1a1a !important; |
|
|
font-family: 'SF Mono', Monaco, 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) |
|
|
|
|
|
|
|
|
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: #f5f5f5 !important; |
|
|
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; |
|
|
background: #e8e8e8 !important; |
|
|
padding: 16px; |
|
|
border-radius: 8px; |
|
|
}} |
|
|
|
|
|
.shadow-box {{ |
|
|
width: 100px; |
|
|
height: 100px; |
|
|
background: #ffffff !important; |
|
|
border-radius: 8px; |
|
|
margin-bottom: 12px; |
|
|
}} |
|
|
|
|
|
.shadow-label {{ |
|
|
font-size: 13px; |
|
|
font-weight: 600; |
|
|
color: #1a1a1a !important; |
|
|
margin-bottom: 4px; |
|
|
}} |
|
|
|
|
|
.shadow-value {{ |
|
|
font-size: 10px; |
|
|
color: #444 !important; |
|
|
font-family: 'SF Mono', Monaco, monospace; |
|
|
text-align: center; |
|
|
word-break: break-all; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div class="shadows-asis-preview"> |
|
|
{rows_html} |
|
|
</div> |
|
|
''' |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
""" |
|
|
|
|
|
|
|
|
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]) |
|
|
|
|
|
|
|
|
rows_html = "" |
|
|
for name, token, size_num in sorted_tokens[:15]: |
|
|
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") |
|
|
|
|
|
|
|
|
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_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") |
|
|
|
|
|
|
|
|
display_name = name.replace("_", " ").replace("-", " ").title() |
|
|
if len(display_name) > 15: |
|
|
display_name = display_name[:15] + "..." |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 [] |
|
|
|
|
|
|
|
|
r, g, b = [x / 255 for x in rgb] |
|
|
h, l, s = colorsys.rgb_to_hls(r, g, b) |
|
|
|
|
|
|
|
|
|
|
|
shade_lightness = { |
|
|
50: 0.95, |
|
|
100: 0.90, |
|
|
200: 0.80, |
|
|
300: 0.70, |
|
|
400: 0.60, |
|
|
500: l, |
|
|
600: 0.45, |
|
|
700: 0.35, |
|
|
800: 0.25, |
|
|
900: 0.15, |
|
|
950: 0.08, |
|
|
} |
|
|
|
|
|
|
|
|
ramp = [] |
|
|
for shade, target_l in shade_lightness.items(): |
|
|
|
|
|
if target_l > 0.8: |
|
|
adjusted_s = s * 0.6 |
|
|
elif target_l < 0.2: |
|
|
adjusted_s = s * 0.8 |
|
|
else: |
|
|
adjusted_s = s |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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_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", |
|
|
max_colors: int = 20 |
|
|
) -> str: |
|
|
""" |
|
|
Generate HTML preview for color ramps. |
|
|
|
|
|
Sorts colors by frequency and filters out near-white/near-black |
|
|
to prioritize showing actual brand colors. |
|
|
|
|
|
Args: |
|
|
color_tokens: Dict of colors {name: {value: "#hex", ...}} |
|
|
background: Background color |
|
|
max_colors: Maximum colors to show ramps for |
|
|
|
|
|
Returns: |
|
|
HTML string for Gradio HTML component |
|
|
""" |
|
|
|
|
|
def get_color_priority(name, token): |
|
|
"""Calculate priority score for a color (higher = more important).""" |
|
|
if isinstance(token, dict): |
|
|
hex_val = token.get("value", "#888888") |
|
|
frequency = token.get("frequency", 0) |
|
|
else: |
|
|
hex_val = str(token) |
|
|
frequency = 0 |
|
|
|
|
|
|
|
|
if not hex_val.startswith("#"): |
|
|
hex_val = f"#{hex_val}" |
|
|
|
|
|
|
|
|
try: |
|
|
r = int(hex_val[1:3], 16) |
|
|
g = int(hex_val[3:5], 16) |
|
|
b = int(hex_val[5:7], 16) |
|
|
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 |
|
|
|
|
|
|
|
|
max_c = max(r, g, b) |
|
|
min_c = min(r, g, b) |
|
|
saturation = (max_c - min_c) / 255 if max_c > 0 else 0 |
|
|
except: |
|
|
luminance = 0.5 |
|
|
saturation = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
score = frequency * 10 |
|
|
|
|
|
|
|
|
if luminance > 0.9: |
|
|
score -= 500 |
|
|
if luminance < 0.1: |
|
|
score -= 300 |
|
|
|
|
|
|
|
|
score += saturation * 200 |
|
|
|
|
|
|
|
|
name_lower = name.lower() |
|
|
if any(kw in name_lower for kw in ['primary', 'brand', 'accent', 'cyan', 'blue', 'green', 'red', 'orange', 'purple']): |
|
|
score += 100 |
|
|
|
|
|
|
|
|
if any(kw in name_lower for kw in ['background', 'border', 'neutral', 'gray', 'grey']): |
|
|
score -= 50 |
|
|
|
|
|
return score |
|
|
|
|
|
|
|
|
sorted_colors = [] |
|
|
for name, token in color_tokens.items(): |
|
|
priority = get_color_priority(name, token) |
|
|
sorted_colors.append((name, token, priority)) |
|
|
|
|
|
sorted_colors.sort(key=lambda x: -x[2]) |
|
|
|
|
|
rows_html = "" |
|
|
shown_count = 0 |
|
|
|
|
|
for name, token, priority in sorted_colors: |
|
|
if shown_count >= max_colors: |
|
|
break |
|
|
|
|
|
|
|
|
if isinstance(token, dict): |
|
|
hex_val = token.get("value", "#888888") |
|
|
else: |
|
|
hex_val = str(token) |
|
|
|
|
|
|
|
|
if not hex_val.startswith("#"): |
|
|
hex_val = f"#{hex_val}" |
|
|
|
|
|
|
|
|
if len(hex_val) < 7: |
|
|
continue |
|
|
|
|
|
|
|
|
ramp = generate_color_ramp(hex_val) |
|
|
if not ramp: |
|
|
continue |
|
|
|
|
|
|
|
|
display_name = name.replace("_", " ").replace("-", " ").replace(".", " ").title() |
|
|
if len(display_name) > 18: |
|
|
display_name = display_name[:15] + "..." |
|
|
|
|
|
|
|
|
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"] |
|
|
|
|
|
|
|
|
text_color = "#000" if shade < 500 else "#FFF" |
|
|
|
|
|
|
|
|
if aa_white or aa_black: |
|
|
aa_indicator = "✓" |
|
|
aa_class = "aa-pass" |
|
|
else: |
|
|
aa_indicator = "" |
|
|
aa_class = "" |
|
|
|
|
|
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> |
|
|
''' |
|
|
shown_count += 1 |
|
|
|
|
|
|
|
|
total_colors = len(color_tokens) |
|
|
count_info = f"Showing {shown_count} of {total_colors} colors (sorted by brand priority)" |
|
|
|
|
|
html = f''' |
|
|
<style> |
|
|
.color-ramps-preview {{ |
|
|
font-family: system-ui, -apple-system, sans-serif; |
|
|
background: #f5f5f5 !important; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
overflow-x: auto; |
|
|
}} |
|
|
|
|
|
.color-row {{ |
|
|
display: flex; |
|
|
align-items: center; |
|
|
margin-bottom: 16px; |
|
|
padding: 12px; |
|
|
background: #ffffff !important; |
|
|
border-radius: 8px; |
|
|
border: 1px solid #d0d0d0 !important; |
|
|
}} |
|
|
|
|
|
.color-row:last-child {{ |
|
|
margin-bottom: 0; |
|
|
}} |
|
|
|
|
|
.color-info {{ |
|
|
display: flex; |
|
|
align-items: center; |
|
|
min-width: 160px; |
|
|
margin-right: 20px; |
|
|
}} |
|
|
|
|
|
.color-swatch {{ |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
border-radius: 8px; |
|
|
border: 2px solid rgba(0,0,0,0.15) !important; |
|
|
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 !important; |
|
|
margin-bottom: 4px; |
|
|
background: #e0e0e0 !important; |
|
|
padding: 4px 10px; |
|
|
border-radius: 4px; |
|
|
display: inline-block; |
|
|
}} |
|
|
|
|
|
.color-hex {{ |
|
|
font-size: 12px; |
|
|
color: #333 !important; |
|
|
font-family: 'SF Mono', Monaco, monospace; |
|
|
margin-top: 4px; |
|
|
font-weight: 500; |
|
|
}} |
|
|
|
|
|
.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) !important; |
|
|
}} |
|
|
|
|
|
.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: #166534 !important; |
|
|
}} |
|
|
|
|
|
.aa-fail {{ |
|
|
color: #991b1b !important; |
|
|
}} |
|
|
|
|
|
.shade-cell:hover .shade-label, |
|
|
.shade-cell:hover .aa-badge {{ |
|
|
opacity: 1; |
|
|
}} |
|
|
|
|
|
/* Header row */ |
|
|
.ramp-header {{ |
|
|
display: flex; |
|
|
margin-bottom: 12px; |
|
|
padding-left: 180px; |
|
|
background: #e8e8e8 !important; |
|
|
padding-top: 8px; |
|
|
padding-bottom: 8px; |
|
|
border-radius: 6px; |
|
|
}} |
|
|
|
|
|
.ramp-header-label {{ |
|
|
width: 48px; |
|
|
text-align: center; |
|
|
font-size: 12px; |
|
|
font-weight: 700; |
|
|
color: #333 !important; |
|
|
margin-right: 4px; |
|
|
}} |
|
|
|
|
|
.ramps-header-info {{ |
|
|
font-size: 14px; |
|
|
color: #333 !important; |
|
|
margin-bottom: 16px; |
|
|
padding: 10px 14px; |
|
|
background: #e0e0e0 !important; |
|
|
border-radius: 6px; |
|
|
font-weight: 500; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div class="color-ramps-preview"> |
|
|
<div class="ramps-header-info">{count_info}</div> |
|
|
<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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_semantic_color_ramps_html( |
|
|
semantic_analysis: dict, |
|
|
color_tokens: dict, |
|
|
llm_recommendations: dict = None, |
|
|
background: str = "#F5F5F5" |
|
|
) -> str: |
|
|
""" |
|
|
Generate HTML preview for colors organized by semantic role with LLM recommendations. |
|
|
|
|
|
Args: |
|
|
semantic_analysis: Output from SemanticColorAnalyzer |
|
|
color_tokens: Dict of all color tokens |
|
|
llm_recommendations: LLM suggestions for color improvements |
|
|
background: Background color |
|
|
|
|
|
Returns: |
|
|
HTML string for Gradio HTML component |
|
|
""" |
|
|
|
|
|
def generate_single_ramp(hex_val: str) -> str: |
|
|
"""Generate a single color ramp HTML.""" |
|
|
ramp = generate_color_ramp(hex_val) |
|
|
if not ramp: |
|
|
return "" |
|
|
|
|
|
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"] |
|
|
|
|
|
text_color = "#000" if shade < 500 else "#FFF" |
|
|
aa_indicator = "✓" if aa_white or aa_black else "" |
|
|
|
|
|
shades_html += f''' |
|
|
<div class="sem-shade" style="background-color: {hex_color};"> |
|
|
<span class="sem-shade-num" style="color: {text_color};">{shade}</span> |
|
|
<span class="sem-shade-aa" style="color: {text_color};">{aa_indicator}</span> |
|
|
</div> |
|
|
''' |
|
|
return shades_html |
|
|
|
|
|
def color_row_with_recommendation(hex_val: str, role: str, role_display: str, recommendation: dict = None) -> str: |
|
|
"""Generate a color row with optional LLM recommendation.""" |
|
|
ramp_html = generate_single_ramp(hex_val) |
|
|
|
|
|
|
|
|
try: |
|
|
from core.color_utils import get_contrast_with_white |
|
|
contrast = get_contrast_with_white(hex_val) |
|
|
aa_status = "✓ AA" if contrast >= 4.5 else f"⚠️ {contrast:.1f}:1" |
|
|
aa_class = "aa-ok" if contrast >= 4.5 else "aa-warn" |
|
|
except: |
|
|
aa_status = "" |
|
|
aa_class = "" |
|
|
|
|
|
|
|
|
rec_html = "" |
|
|
if recommendation: |
|
|
suggested = recommendation.get("suggested", "") |
|
|
issue = recommendation.get("issue", "") |
|
|
if suggested and suggested != hex_val: |
|
|
rec_html = f''' |
|
|
<div class="llm-rec"> |
|
|
<span class="rec-label">💡 LLM:</span> |
|
|
<span class="rec-issue">{issue}</span> |
|
|
<span class="rec-arrow">→</span> |
|
|
<span class="rec-suggested" style="background-color: {suggested};">{suggested}</span> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
return f''' |
|
|
<div class="sem-color-row"> |
|
|
<div class="sem-color-info"> |
|
|
<div class="sem-swatch" style="background-color: {hex_val};"></div> |
|
|
<div class="sem-details"> |
|
|
<div class="sem-role">{role_display}</div> |
|
|
<div class="sem-hex">{hex_val} <span class="{aa_class}">{aa_status}</span></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="sem-ramp">{ramp_html}</div> |
|
|
{rec_html} |
|
|
</div> |
|
|
''' |
|
|
|
|
|
def category_section(title: str, icon: str, colors: dict, category_key: str) -> str: |
|
|
"""Generate a category section with color rows.""" |
|
|
if not colors: |
|
|
return "" |
|
|
|
|
|
rows_html = "" |
|
|
for role, data in colors.items(): |
|
|
if data and isinstance(data, dict) and "hex" in data: |
|
|
|
|
|
rec = None |
|
|
if llm_recommendations: |
|
|
color_recs = llm_recommendations.get("color_recommendations", {}) |
|
|
rec = color_recs.get(f"{category_key}.{role}", {}) |
|
|
|
|
|
role_display = role.replace("_", " ").title() |
|
|
rows_html += color_row_with_recommendation( |
|
|
data["hex"], |
|
|
f"{category_key}.{role}", |
|
|
role_display, |
|
|
rec |
|
|
) |
|
|
|
|
|
if not rows_html: |
|
|
return "" |
|
|
|
|
|
return f''' |
|
|
<div class="sem-category"> |
|
|
<h3 class="sem-cat-title">{icon} {title}</h3> |
|
|
{rows_html} |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
if not semantic_analysis: |
|
|
return ''' |
|
|
<div style="padding: 40px; text-align: center; background: #fff3cd !important; border-radius: 8px;"> |
|
|
<p style="color: #856404 !important; font-size: 14px;">⚠️ No semantic analysis available.</p> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
|
|
|
sections_html = "" |
|
|
sections_html += category_section("Brand Colors", "🎨", semantic_analysis.get("brand", {}), "brand") |
|
|
sections_html += category_section("Text Colors", "📝", semantic_analysis.get("text", {}), "text") |
|
|
sections_html += category_section("Background Colors", "🖼️", semantic_analysis.get("background", {}), "background") |
|
|
sections_html += category_section("Border Colors", "📏", semantic_analysis.get("border", {}), "border") |
|
|
sections_html += category_section("Feedback Colors", "🚨", semantic_analysis.get("feedback", {}), "feedback") |
|
|
|
|
|
|
|
|
llm_summary = "" |
|
|
if llm_recommendations: |
|
|
changes = llm_recommendations.get("changes_made", []) |
|
|
if changes: |
|
|
changes_html = "".join([f"<li>{c}</li>" for c in changes[:5]]) |
|
|
llm_summary = f''' |
|
|
<div class="llm-summary"> |
|
|
<h4>🤖 LLM Recommendations Applied:</h4> |
|
|
<ul>{changes_html}</ul> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
html = f''' |
|
|
<style> |
|
|
.sem-ramps-preview {{ |
|
|
font-family: system-ui, -apple-system, sans-serif; |
|
|
background: #f5f5f5 !important; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
}} |
|
|
|
|
|
.sem-category {{ |
|
|
background: #ffffff !important; |
|
|
border-radius: 8px; |
|
|
padding: 16px; |
|
|
margin-bottom: 20px; |
|
|
border: 1px solid #d0d0d0 !important; |
|
|
}} |
|
|
|
|
|
.sem-cat-title {{ |
|
|
font-size: 16px; |
|
|
font-weight: 700; |
|
|
color: #1a1a1a !important; |
|
|
margin: 0 0 16px 0; |
|
|
padding-bottom: 8px; |
|
|
border-bottom: 2px solid #e0e0e0 !important; |
|
|
}} |
|
|
|
|
|
.sem-color-row {{ |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
align-items: center; |
|
|
padding: 12px; |
|
|
background: #f8f8f8 !important; |
|
|
border-radius: 6px; |
|
|
margin-bottom: 12px; |
|
|
border: 1px solid #e0e0e0 !important; |
|
|
}} |
|
|
|
|
|
.sem-color-row:last-child {{ |
|
|
margin-bottom: 0; |
|
|
}} |
|
|
|
|
|
.sem-color-info {{ |
|
|
display: flex; |
|
|
align-items: center; |
|
|
min-width: 180px; |
|
|
margin-right: 16px; |
|
|
}} |
|
|
|
|
|
.sem-swatch {{ |
|
|
width: 48px; |
|
|
height: 48px; |
|
|
border-radius: 8px; |
|
|
border: 2px solid rgba(0,0,0,0.15) !important; |
|
|
margin-right: 12px; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
|
}} |
|
|
|
|
|
.sem-details {{ |
|
|
flex: 1; |
|
|
}} |
|
|
|
|
|
.sem-role {{ |
|
|
font-weight: 700; |
|
|
font-size: 14px; |
|
|
color: #1a1a1a !important; |
|
|
margin-bottom: 4px; |
|
|
}} |
|
|
|
|
|
.sem-hex {{ |
|
|
font-size: 12px; |
|
|
font-family: 'SF Mono', Monaco, monospace; |
|
|
color: #333 !important; |
|
|
}} |
|
|
|
|
|
.aa-ok {{ |
|
|
color: #166534 !important; |
|
|
font-weight: 600; |
|
|
}} |
|
|
|
|
|
.aa-warn {{ |
|
|
color: #b45309 !important; |
|
|
font-weight: 600; |
|
|
}} |
|
|
|
|
|
.sem-ramp {{ |
|
|
display: flex; |
|
|
gap: 3px; |
|
|
flex: 1; |
|
|
min-width: 400px; |
|
|
}} |
|
|
|
|
|
.sem-shade {{ |
|
|
width: 36px; |
|
|
height: 36px; |
|
|
border-radius: 4px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
border: 1px solid rgba(0,0,0,0.1) !important; |
|
|
}} |
|
|
|
|
|
.sem-shade-num {{ |
|
|
font-size: 9px; |
|
|
font-weight: 700; |
|
|
}} |
|
|
|
|
|
.sem-shade-aa {{ |
|
|
font-size: 10px; |
|
|
}} |
|
|
|
|
|
.llm-rec {{ |
|
|
width: 100%; |
|
|
margin-top: 10px; |
|
|
padding: 8px 12px; |
|
|
background: #fef3c7 !important; |
|
|
border-radius: 4px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
border: 1px solid #f59e0b !important; |
|
|
}} |
|
|
|
|
|
.rec-label {{ |
|
|
font-weight: 600; |
|
|
color: #92400e !important; |
|
|
}} |
|
|
|
|
|
.rec-issue {{ |
|
|
color: #78350f !important; |
|
|
font-size: 13px; |
|
|
}} |
|
|
|
|
|
.rec-arrow {{ |
|
|
color: #92400e !important; |
|
|
}} |
|
|
|
|
|
.rec-suggested {{ |
|
|
padding: 4px 10px; |
|
|
border-radius: 4px; |
|
|
font-family: 'SF Mono', Monaco, monospace; |
|
|
font-size: 12px; |
|
|
font-weight: 600; |
|
|
color: #fff !important; |
|
|
text-shadow: 0 1px 2px rgba(0,0,0,0.3); |
|
|
}} |
|
|
|
|
|
.llm-summary {{ |
|
|
background: #dbeafe !important; |
|
|
border: 1px solid #3b82f6 !important; |
|
|
border-radius: 8px; |
|
|
padding: 16px; |
|
|
margin-top: 20px; |
|
|
}} |
|
|
|
|
|
.llm-summary h4 {{ |
|
|
color: #1e40af !important; |
|
|
margin: 0 0 12px 0; |
|
|
font-size: 14px; |
|
|
}} |
|
|
|
|
|
.llm-summary ul {{ |
|
|
margin: 0; |
|
|
padding-left: 20px; |
|
|
color: #1e3a8a !important; |
|
|
}} |
|
|
|
|
|
.llm-summary li {{ |
|
|
margin-bottom: 4px; |
|
|
font-size: 13px; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div class="sem-ramps-preview"> |
|
|
{sections_html} |
|
|
{llm_summary} |
|
|
</div> |
|
|
''' |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|