Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| ) |