"""
F1 Predictor 2026 - UI Components
All components. Colors: skyblue, darkblue, darkgray, black, red, green.
NO WHITE TEXT ANYWHERE.
"""
import streamlit as st
def hero(title: str, subtitle: str, kpis: list[dict] = None) -> None:
"""Hero section with accent bar and KPI cards."""
kpis = kpis or []
kpi_html = "".join(
f"""
{k.get('value','-')}
{k.get('label','')}
"""
for k in kpis
)
st.markdown(
f"""
{title}
{subtitle}
{kpi_html}
""",
unsafe_allow_html=True,
)
def section_title(text: str) -> None:
"""Section title with skyblue accent bar."""
st.markdown(
f"""
{text}
""",
unsafe_allow_html=True,
)
def divider() -> None:
"""Styled horizontal divider."""
st.markdown(
'
',
unsafe_allow_html=True,
)
def card(title: str, content: str, icon: str = "📊") -> None:
"""Card with icon header."""
st.markdown(
f"""""",
unsafe_allow_html=True,
)
def metric_card(label: str, value: str, delta: str = "", accent: str = "#3B82F6") -> None:
"""Individual metric card."""
delta_html = f'{delta}
' if delta else ""
st.markdown(
f"""
{value}
{label}
{delta_html}
""",
unsafe_allow_html=True,
)
def metrics_grid(metrics: list[dict]) -> None:
"""Grid of metric cards."""
grid = "".join(
f"""
{m.get('value','-')}
{m.get('label','')}
"""
for m in metrics
)
st.markdown(
f"""
{grid}
""",
unsafe_allow_html=True,
)
def data_table(headers: list[str], rows: list[list]) -> None:
"""Custom styled data table."""
th = "".join(f"{h} " for h in headers)
trs = ""
for row in rows:
cells = "".join(f"{c} " for c in row)
trs += f"{cells} "
st.markdown(
f"""""",
unsafe_allow_html=True,
)
def stat_row(label: str, value: str, highlight: bool = False) -> None:
"""Label-value row."""
vcolor = "#3B82F6" if highlight else "#0F172A"
st.markdown(
f"""
{label}
{value}
""",
unsafe_allow_html=True,
)
def progress_bar(value: float, max_val: float = 100.0, label: str = "") -> None:
"""Progress bar with label."""
pct = min(100.0, max(0.0, (value / max_val) * 100))
lbl = f'{label}
' if label else ""
st.markdown(
f"""{lbl}
0% {pct:.0f}%
""",
unsafe_allow_html=True,
)
def podium(items: list[dict], max_items: int = 3) -> None:
"""Podium list with gold/silver/bronze styling."""
borders = ["#FBBF24", "#9CA3AF", "#D97706"]
bgs = ["rgba(251,191,36,0.1)", "rgba(156,163,175,0.1)", "rgba(217,119,6,0.1)"]
medals = ["🥇", "🥈", "🥉"]
html = ""
for i, item in enumerate(items[:max_items]):
border = borders[i] if i < 3 else "#E2E8F0"
bg = bgs[i] if i < 3 else "#F1F5F9"
medal = medals[i] if i < 3 else f"P{i+1}"
html += f"""
{medal}
{item.get('name','Unknown')}
{item.get('team','')}
{item.get('stat','-')}
"""
st.markdown(f'{html}
', unsafe_allow_html=True)
def alert(message: str, alert_type: str = "info") -> None:
"""Alert box: info, success, warning, error."""
colors = {
"success": ("#10B981", "rgba(16,185,129,0.1)"),
"warning": ("#F59E0B", "rgba(245,158,11,0.1)"),
"error": ("#EF4444", "rgba(239,68,68,0.1)"),
"info": ("#06B6D4", "rgba(6,182,212,0.1)"),
}
border, bg = colors.get(alert_type, colors["info"])
st.markdown(
f"""{message}
""",
unsafe_allow_html=True,
)
def weather(condition: str = "dry") -> None:
"""Weather widget."""
data = {
"dry": {"icon": "☀️", "temp": "24°C", "desc": "Sunny & Clear"},
"mixed": {"icon": "🌥️", "temp": "18°C", "desc": "Partly Cloudy"},
"wet": {"icon": "🌧️", "temp": "14°C", "desc": "Rain Expected"},
}.get(condition, {"icon": "☀️", "temp": "24°C", "desc": "Sunny & Clear"})
st.markdown(
f"""
{data['icon']}
{data['temp']}
{data['desc']}
""",
unsafe_allow_html=True,
)
def team_badge(team_name: str, color: str) -> None:
"""Team badge with color dot."""
st.markdown(
f"""
{team_name} """,
unsafe_allow_html=True,
)
def empty_state(icon: str, title: str, description: str = "") -> None:
"""Empty state placeholder."""
desc = f'{description}
' if description else ""
st.markdown(
f"""""",
unsafe_allow_html=True,
)
def session_tabs(active: str = "race") -> str:
"""Friday/Saturday/Sunday session selector. Returns active key."""
sessions = {
"practice": {"icon": "🏁", "day": "Friday Practice", "desc": "FP1 · FP2 · FP3"},
"qualifying": {"icon": "⚡", "day": "Saturday Qualifying", "desc": "Q1 · Q2 · Q3"},
"race": {"icon": "🏆", "day": "Sunday Grand Prix", "desc": "Full Race"},
}
tabs = ""
for key, data in sessions.items():
is_active = key == active
bg = "linear-gradient(135deg,#3B82F6,#2563EB)" if is_active else "#F1F5F9"
border = "#3B82F6" if is_active else "#E2E8F0"
title_color = "#F8FAFC" if is_active else "#0F172A"
desc_color = "rgba(248,250,252,0.8)" if is_active else "#94A3B8"
shadow = "0 4px 12px rgba(59,130,246,0.25)" if is_active else "none"
tabs += f"""
{data['icon']}
{data['day']}
{data['desc']}
"""
st.markdown(
f"""{tabs}
""",
unsafe_allow_html=True,
)
return active
def navbar(active_page: str = "dashboard") -> None:
"""Top navigation bar."""
pages = {
"dashboard": "Dashboard", "h2h": "H2H", "constructor": "Constructor",
"championship": "Championship", "accuracy": "Accuracy", "download": "Download",
}
links = ""
for key, name in pages.items():
is_active = key == active_page
bg = "#1E3A8A" if is_active else "transparent"
color = "#F8FAFC" if is_active else "#475569"
hover = "#F1F5F9" if not is_active else "#1E3A8A"
links += f"""{name} """
st.markdown(
f"""
F1
Predictor 2026
{links}
""",
unsafe_allow_html=True,
)