Spaces:
Sleeping
Sleeping
| """ | |
| F1 Predictor 2026 - Theme System | |
| Consolidated: colors, CSS, page config, plotly helpers, team colors | |
| """ | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| # ── Color Palette ─────────────────────────────────────────── | |
| # STRICT: skyblue, darkblue, darkgray, black, red, green ONLY | |
| COLORS = { | |
| "skyblue": "#3B82F6", | |
| "skyblue_dark": "#2563EB", | |
| "skyblue_light": "#60A5FA", | |
| "darkblue": "#1E3A8A", | |
| "darkgray": "#475569", | |
| "black": "#0F172A", | |
| "red": "#EF4444", | |
| "green": "#10B981", | |
| "amber": "#F59E0B", | |
| "cyan": "#06B6D4", | |
| "gold": "#FBBF24", | |
| "silver": "#9CA3AF", | |
| "bronze": "#D97706", | |
| "bg": "#F8FAFC", | |
| "bg_card": "#F1F5F9", | |
| "border": "#E2E8F0", | |
| "text": "#0F172A", | |
| "text_secondary": "#475569", | |
| "text_muted": "#94A3B8", | |
| "team_red_bull": "#1E41FF", | |
| "team_ferrari": "#DC0000", | |
| "team_mercedes": "#00D2BE", | |
| "team_mclaren": "#FF8000", | |
| "team_aston_martin": "#006F62", | |
| "team_alpine": "#0093CC", | |
| "team_williams": "#005AFF", | |
| "team_rb": "#2B4562", | |
| "team_kick_sauber": "#00E701", | |
| "team_haas": "#B6BABD", | |
| } | |
| TEAM_COLOR_MAP = { | |
| "red_bull": "#1E41FF", "ferrari": "#DC0000", "mercedes": "#00D2BE", | |
| "mclaren": "#FF8000", "aston_martin": "#006F62", "alpine": "#0093CC", | |
| "williams": "#005AFF", "rb": "#2B4562", "kick_sauber": "#00E701", | |
| "haas": "#B6BABD", "red bull": "#1E41FF", "aston martin": "#006F62", | |
| "kick sauber": "#00E701", "racing bulls": "#2B4562", "sauber": "#00E701", | |
| "visa cash app rb": "#2B4562", | |
| } | |
| def bar_colors_from_teams(predictions): | |
| """Generate bar colors based on team affiliation.""" | |
| return [TEAM_COLOR_MAP.get( | |
| p.get("team", "").lower().strip().replace(" ", "_").replace("-", "_"), | |
| "#3B82F6" | |
| ) for p in predictions] | |
| def f1_plotly_layout(title="", show_legend=False): | |
| """Return a Plotly layout dict styled for the F1 light theme.""" | |
| return { | |
| "paper_bgcolor": "rgba(0,0,0,0)", | |
| "plot_bgcolor": "rgba(0,0,0,0)", | |
| "font": {"color": "#0F172A", "family": "Inter, sans-serif"}, | |
| "title": {"text": title, "font": {"color": "#0F172A", "size": 18}}, | |
| "xaxis": { | |
| "gridcolor": "rgba(71,85,105,0.15)", "linecolor": "rgba(71,85,105,0.3)", | |
| "tickfont": {"color": "#475569"}, "title": {"font": {"color": "#475569"}}, | |
| }, | |
| "yaxis": { | |
| "gridcolor": "rgba(71,85,105,0.15)", "linecolor": "rgba(71,85,105,0.3)", | |
| "tickfont": {"color": "#475569"}, "title": {"font": {"color": "#475569"}}, | |
| }, | |
| "legend": {"font": {"color": "#475569"}, "bgcolor": "rgba(0,0,0,0)"} if show_legend else {}, | |
| "margin": {"l": 60, "r": 20, "t": 60, "b": 60}, | |
| } | |
| # ── Complete CSS ────────────────────────────────────────── | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); | |
| html, body, .stApp, [data-testid="stAppViewContainer"], .main, .block-container { | |
| background: #F8FAFC !important; background-color: #F8FAFC !important; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, sans-serif; color: #0F172A; line-height: 1.6; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| #MainMenu, header, footer, .stApp > header, [data-testid="stToolbar"] { | |
| display: none !important; | |
| } | |
| /* Top Navigation Bar */ | |
| .top-nav-bar { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1rem; | |
| background: #F8FAFC; | |
| border-bottom: 1px solid #E2E8F0; | |
| padding: 0.75rem 2rem; | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| backdrop-filter: blur(10px); | |
| } | |
| .nav-item { | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| transition: all 0.2s; | |
| color: #475569; | |
| border: 1px solid transparent; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .nav-item.active { | |
| background: linear-gradient(135deg, #3B82F6, #2563EB); | |
| color: #F8FAFC; | |
| border: 1px solid #3B82F6; | |
| } | |
| .nav-item:hover:not(.active) { | |
| background: #F1F5F9; | |
| border: 1px solid #E2E8F0; | |
| } | |
| /* Buttons */ | |
| .stButton > button { | |
| background: linear-gradient(135deg, #3B82F6, #2563EB) !important; | |
| color: #F8FAFC !important; border: none !important; border-radius: 8px !important; | |
| padding: 0.6rem 1.5rem !important; font-weight: 600 !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 4px 12px rgba(59,130,246,0.3) !important; | |
| } | |
| /* Inputs */ | |
| .stSelectbox > div > div, .stNumberInput > div > div, .stSlider > div > div { | |
| background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important; | |
| border-radius: 8px !important; | |
| } | |
| .stSelectbox label, .stNumberInput label, .stSlider label, .stCheckbox label { | |
| color: #475569 !important; font-weight: 600 !important; | |
| } | |
| /* Metrics */ | |
| [data-testid="stMetric"] { | |
| background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important; | |
| border-radius: 10px !important; padding: 1rem !important; | |
| } | |
| [data-testid="stMetric"] label { color: #94A3B8 !important; font-size: 0.75rem !important; text-transform: uppercase; letter-spacing: 0.5px; } | |
| [data-testid="stMetric"] .css-1xarl3l { color: #0F172A !important; font-size: 1.5rem !important; font-weight: 800 !important; } | |
| /* Tabs */ | |
| .stTabs [data-baseweb="tab-list"] { | |
| background: #F1F5F9 !important; border-radius: 10px 10px 0 0 !important; | |
| border-bottom: 2px solid #E2E8F0 !important; | |
| } | |
| .stTabs [data-baseweb="tab"] { color: #94A3B8 !important; font-weight: 500 !important; } | |
| .stTabs [aria-selected="true"] { color: #3B82F6 !important; border-bottom: 2px solid #3B82F6 !important; } | |
| /* DataFrames */ | |
| .stDataFrame { | |
| background: #F1F5F9 !important; border-radius: 10px !important; | |
| border: 1px solid #E2E8F0 !important; | |
| } | |
| .stDataFrame th { background: #E2E8F0 !important; color: #0F172A !important; font-weight: 700 !important; } | |
| .stDataFrame td { color: #1E293B !important; } | |
| .stDataFrame tr:hover td { background: #F8FAFC !important; } | |
| /* Expanders */ | |
| .stExpander { | |
| background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important; | |
| border-radius: 10px !important; | |
| } | |
| /* Sidebar */ | |
| section[data-testid="stSidebar"] { | |
| background: #F1F5F9 !important; border-right: 1px solid #E2E8F0 !important; | |
| } | |
| section[data-testid="stSidebar"] .stMarkdown, section[data-testid="stSidebar"] label { | |
| color: #0F172A !important; | |
| } | |
| /* Sidebar Toggle Button - POSITIONED & VISIBLE */ | |
| button[kind="icon"][data-testid="collapsedControl"], | |
| [data-testid="collapsedControl"], | |
| .stSidebarToggle { | |
| background-color: #EF4444 !important; | |
| border: 2px solid #DC2626 !important; | |
| border-radius: 6px !important; | |
| box-shadow: 0 2px 8px rgba(239,68,68,0.3) !important; | |
| position: fixed !important; | |
| top: 10px !important; | |
| left: 250px !important; | |
| z-index: 9999 !important; | |
| width: 40px !important; | |
| height: 40px !important; | |
| min-width: 40px !important; | |
| min-height: 40px !important; | |
| padding: 8px !important; | |
| opacity: 1 !important; | |
| visibility: visible !important; | |
| } | |
| button[kind="icon"][data-testid="collapsedControl"]:hover, | |
| [data-testid="collapsedControl"]:hover, | |
| .stSidebarToggle:hover { | |
| background-color: #DC2626 !important; | |
| transform: scale(1.05); | |
| } | |
| button[kind="icon"][data-testid="collapsedControl"] svg, | |
| [data-testid="collapsedControl"] svg, | |
| .stSidebarToggle svg { | |
| fill: #FFFFFF !important; | |
| stroke: #FFFFFF !important; | |
| } | |
| /* Radio */ | |
| .stRadio > div { background: #F1F5F9 !important; border-radius: 8px !important; padding: 0.5rem !important; } | |
| .stRadio label span { color: #475569 !important; } | |
| /* Alerts */ | |
| .stAlert { background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important; border-radius: 10px !important; } | |
| .stAlert [data-testid="stAlertContent"] { color: #0F172A !important; } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-track { background: #F8FAFC; } | |
| ::-webkit-scrollbar-thumb { background: #CBD5E0; border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #94A3B8; } | |
| /* Plotly */ | |
| .js-plotly-plot .plotly { border-radius: 10px; } | |
| """ | |
| def apply_theme(): | |
| """Inject theme CSS into Streamlit.""" | |
| js_code = """ | |
| <script> | |
| (function() { | |
| function adjustSidebarTogglePosition() { | |
| // Find the sidebar toggle button | |
| const toggleButtons = document.querySelectorAll('button[kind="icon"][data-testid="collapsedControl"]'); | |
| const sidebar = document.querySelector('section[data-testid="stSidebar"]'); | |
| if (toggleButtons.length > 0 && sidebar) { | |
| const toggle = toggleButtons[0]; | |
| // Get sidebar width to determine if it's expanded or collapsed | |
| const computedStyle = window.getComputedStyle(sidebar); | |
| const sidebarWidth = parseFloat(computedStyle.width); | |
| // If sidebar width is small (~4.5rem or ~72px), it's collapsed | |
| // If wider (~260px+), it's expanded | |
| if (sidebarWidth > 200) { | |
| // Sidebar is expanded - position toggle at 250px | |
| toggle.style.left = '250px'; | |
| } else { | |
| // Sidebar is collapsed - position toggle at 10px | |
| toggle.style.left = '10px'; | |
| } | |
| } | |
| } | |
| // Run immediately | |
| adjustSidebarTogglePosition(); | |
| // Run when the page loads | |
| window.addEventListener('load', adjustSidebarTogglePosition); | |
| // Run periodically to catch sidebar state changes | |
| setInterval(adjustSidebarTogglePosition, 500); | |
| // Watch for DOM mutations that might affect sidebar | |
| const observer = new MutationObserver(function(mutations) { | |
| mutations.forEach(function(mutation) { | |
| if (mutation.type === 'attributes' && | |
| (mutation.attributeName === 'style' || mutation.target.matches('[data-testid="stSidebar"]'))) { | |
| setTimeout(adjustSidebarTogglePosition, 100); | |
| } | |
| }); | |
| }); | |
| observer.observe(document.body, { | |
| attributes: true, | |
| subtree: true, | |
| attributeFilter: ['style', 'class'] | |
| }); | |
| })(); | |
| </script> | |
| """ | |
| st.markdown(f"<style>{CSS}</style>", unsafe_allow_html=True) | |
| st.components.v1.html(js_code, height=0) | |
| def apply_page_config(): | |
| """Set Streamlit page config.""" | |
| st.set_page_config( | |
| page_title="F1 Predictor 2026", | |
| page_icon="🏎️", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) |