FINESE_SCHOOL / src /ui /components.py
Jack-ki1's picture
Upload 47 files
29ca14e verified
Raw
History Blame Contribute Delete
13.1 kB
"""
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"""<div style="background:#F1F5F9;border:1px solid #E2E8F0;border-left:4px solid #3B82F6;
border-radius:10px;padding:1.2rem;transition:all 0.2s;">
<div style="color:#3B82F6;font-size:2rem;font-weight:800;">{k.get('value','-')}</div>
<div style="color:#94A3B8;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;">
{k.get('label','')}</div></div>"""
for k in kpis
)
st.markdown(
f"""<div style="background:#F8FAFC;border:1px solid #E2E8F0;border-radius:16px;
padding:2.5rem;margin-bottom:1.5rem;position:relative;overflow:hidden;">
<div style="position:absolute;top:0;left:0;right:0;height:3px;
background:linear-gradient(90deg,#3B82F6,#60A5FA,#3B82F6);"></div>
<h1 style="color:#0F172A;font-size:2.5rem;font-weight:800;margin-bottom:0.5rem;letter-spacing:-0.5px;">
{title}</h1>
<p style="color:#475569;font-size:1.1rem;margin-bottom:1.5rem;">{subtitle}</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;">
{kpi_html}</div></div>""",
unsafe_allow_html=True,
)
def section_title(text: str) -> None:
"""Section title with skyblue accent bar."""
st.markdown(
f"""<div style="display:flex;align-items:center;gap:0.5rem;color:#0F172A;
font-weight:700;font-size:1.25rem;margin:1.5rem 0 1rem 0;">
<span style="width:4px;height:1.25rem;background:linear-gradient(180deg,#3B82F6,#2563EB);border-radius:2px;">
</span>{text}</div>""",
unsafe_allow_html=True,
)
def divider() -> None:
"""Styled horizontal divider."""
st.markdown(
'<div style="height:1px;background:linear-gradient(90deg,transparent,#E2E8F0,transparent);margin:1.5rem 0;"></div>',
unsafe_allow_html=True,
)
def card(title: str, content: str, icon: str = "📊") -> None:
"""Card with icon header."""
st.markdown(
f"""<div style="background:#F1F5F9;border:1px solid #E2E8F0;border-radius:10px;
box-shadow:0 1px 3px rgba(0,0,0,0.05);padding:1.25rem;margin-bottom:1rem;">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
<span style="font-size:1.25rem;">{icon}</span>
<span style="color:#0F172A;font-weight:700;font-size:1.1rem;">{title}</span></div>
<div style="color:#1E293B;">{content}</div></div>""",
unsafe_allow_html=True,
)
def metric_card(label: str, value: str, delta: str = "", accent: str = "#3B82F6") -> None:
"""Individual metric card."""
delta_html = f'<div style="color:#10B981;font-size:0.85rem;font-weight:600;margin-top:0.25rem;">{delta}</div>' if delta else ""
st.markdown(
f"""<div style="background:#F1F5F9;border:1px solid #E2E8F0;border-radius:10px;padding:1.2rem;text-align:center;">
<div style="color:{accent};font-size:1.75rem;font-weight:800;">{value}</div>
<div style="color:#94A3B8;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.5px;margin-top:0.25rem;">
{label}</div>{delta_html}</div>""",
unsafe_allow_html=True,
)
def metrics_grid(metrics: list[dict]) -> None:
"""Grid of metric cards."""
grid = "".join(
f"""<div style="background:#F1F5F9;border:1px solid #E2E8F0;border-radius:10px;padding:1rem;text-align:center;">
<div style="color:#3B82F6;font-size:1.75rem;font-weight:800;">{m.get('value','-')}</div>
<div style="color:#94A3B8;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.5px;margin-top:0.25rem;">
{m.get('label','')}</div></div>"""
for m in metrics
)
st.markdown(
f"""<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:1rem;">
{grid}</div>""",
unsafe_allow_html=True,
)
def data_table(headers: list[str], rows: list[list]) -> None:
"""Custom styled data table."""
th = "".join(f"<th style='background:#E2E8F0;color:#0F172A;font-weight:700;padding:0.75rem 1rem;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px;'>{h}</th>" for h in headers)
trs = ""
for row in rows:
cells = "".join(f"<td style='padding:0.75rem 1rem;color:#1E293B;border-bottom:1px solid #E2E8F0;'>{c}</td>" for c in row)
trs += f"<tr style='transition:background 0.15s;' onmouseover=\"this.style.background='#F8FAFC'\" onmouseout=\"this.style.background='transparent'\">{cells}</tr>"
st.markdown(
f"""<table style="width:100%;border-collapse:collapse;background:#F1F5F9;border-radius:10px;overflow:hidden;border:1px solid #E2E8F0;">
<thead><tr>{th}</tr></thead><tbody>{trs}</tbody></table>""",
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"""<div style="display:flex;justify-content:space-between;padding:0.75rem 0;border-bottom:1px solid #E2E8F0;">
<span style="color:#475569;font-size:0.9rem;">{label}</span>
<span style="color:{vcolor};font-weight:700;font-size:0.95rem;">{value}</span></div>""",
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'<div style="color:#475569;font-size:0.875rem;margin-bottom:0.25rem;">{label}</div>' if label else ""
st.markdown(
f"""{lbl}<div style="background:#E2E8F0;border-radius:9999px;height:8px;overflow:hidden;">
<div style="height:100%;border-radius:9999px;background:linear-gradient(90deg,#3B82F6,#60A5FA);width:{pct}%;">
</div></div><div style="display:flex;justify-content:space-between;margin-top:0.25rem;font-size:0.75rem;color:#94A3B8;">
<span>0%</span><span>{pct:.0f}%</span></div>""",
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"""<div style="display:flex;align-items:center;gap:1rem;padding:1rem;background:linear-gradient(90deg,{bg},#F1F5F9);
border-radius:10px;border:1px solid #E2E8F0;border-left:4px solid {border};margin-bottom:0.5rem;">
<div style="font-size:1.5rem;width:2.5rem;text-align:center;">{medal}</div>
<div style="flex:1;"><div style="color:#0F172A;font-weight:700;">{item.get('name','Unknown')}</div>
<div style="color:#475569;font-size:0.875rem;">{item.get('team','')}</div></div>
<div style="color:#3B82F6;font-weight:700;font-size:1.1rem;">{item.get('stat','-')}</div></div>"""
st.markdown(f'<div style="display:flex;flex-direction:column;gap:0.5rem;">{html}</div>', 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"""<div style="padding:1rem 1.25rem;border-radius:10px;margin-bottom:1rem;
border-left:4px solid {border};background:{bg};color:#0F172A;">{message}</div>""",
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"""<div style="display:flex;align-items:center;gap:1rem;background:#F1F5F9;
border:1px solid #E2E8F0;border-radius:10px;padding:1rem 1.25rem;">
<div style="font-size:2rem;">{data['icon']}</div>
<div><div style="color:#0F172A;font-size:1.5rem;font-weight:700;">{data['temp']}</div>
<div style="color:#475569;font-size:0.875rem;">{data['desc']}</div></div></div>""",
unsafe_allow_html=True,
)
def team_badge(team_name: str, color: str) -> None:
"""Team badge with color dot."""
st.markdown(
f"""<span style="display:inline-flex;align-items:center;gap:0.375rem;padding:0.25rem 0.625rem;
border-radius:6px;font-size:0.8rem;font-weight:600;background:{color}15;color:{color};">
<span style="width:8px;height:8px;border-radius:50%;background:{color};display:inline-block;"></span>
{team_name}</span>""",
unsafe_allow_html=True,
)
def empty_state(icon: str, title: str, description: str = "") -> None:
"""Empty state placeholder."""
desc = f'<div style="color:#94A3B8;font-size:0.9rem;">{description}</div>' if description else ""
st.markdown(
f"""<div style="text-align:center;padding:3rem 1.5rem;">
<div style="font-size:3rem;margin-bottom:1rem;opacity:0.5;">{icon}</div>
<div style="color:#475569;font-weight:600;font-size:1.1rem;margin-bottom:0.5rem;">{title}</div>
{desc}</div>""",
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"""<div style="flex:1;background:{bg};border:1px solid {border};border-radius:12px;
padding:1rem;text-align:center;transition:all 0.2s;box-shadow:{shadow};cursor:pointer;">
<div style="font-size:1.5rem;margin-bottom:0.25rem;">{data['icon']}</div>
<div style="color:{title_color};font-weight:700;font-size:0.95rem;">{data['day']}</div>
<div style="color:{desc_color};font-size:0.75rem;">{data['desc']}</div></div>"""
st.markdown(
f"""<div style="display:flex;gap:0.75rem;margin-bottom:1.5rem;">{tabs}</div>""",
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"""<button style="background:{bg};border:none;padding:0.5rem 1rem;border-radius:8px;
color:{color};font-weight:500;cursor:pointer;transition:all 0.2s;"
onmouseover="this.style.background='{hover}'"
onmouseout="this.style.background='{bg}'">{name}</button>"""
st.markdown(
f"""<div style="display:flex;align-items:center;justify-content:space-between;
background:#F8FAFC;border-bottom:1px solid #E2E8F0;padding:1rem 2rem;
position:sticky;top:0;z-index:1000;backdrop-filter:blur(10px);">
<div style="display:flex;align-items:center;gap:0.75rem;font-weight:800;font-size:1.25rem;color:#0F172A;">
<span style="background:linear-gradient(135deg,#3B82F6,#2563EB);color:#F8FAFC;
padding:0.25rem 0.6rem;border-radius:6px;font-size:0.875rem;font-weight:800;">F1</span>
<span>Predictor 2026</span></div>
<div style="display:flex;gap:0.5rem;">{links}</div></div>""",
unsafe_allow_html=True,
)