File size: 13,108 Bytes
29ca14e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
"""
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,
    )